Searching and Sorting Algorithms
Linear and binary search, bubble, insertion and merge sort: each one written in Python, traced by hand, and run on real data with a counter so you can see why one beats another.
Linear and binary search
A search looks for one value in a list and reports where it is, or that it is missing.
Linear search
Check each item in turn, starting at the front, until you find it or run out.
def linear_search(items, target):
comparisons = 0
for i in range(len(items)):
comparisons += 1
if items[i] == target:
return i, comparisons
return -1, comparisons
data = [3, 8, 12, 17, 21, 25, 30, 36, 41, 47, 52, 58, 63, 70, 76]
print(linear_search(data, 63)) # (12, 13): found at position 12 after 13 comparisons
print(linear_search(data, 50)) # (-1, 15): not there, so all 15 items were checkedLinear search works on any list, sorted or not. In the worst case it checks every item.
Binary search
Binary search needs the list to be sorted. It looks at the middle item, and that one comparison throws away half of the list: if the target is bigger, everything at or before the middle is ruled out, and if it is smaller, everything at or after it is.
Step 1 of 5 Looking for 63. The list is sorted, so the middle item says which half to throw away.
def binary_search(items, target):
low = 0
high = len(items) - 1
comparisons = 0
while low <= high:
mid = (low + high) // 2
comparisons += 1
if items[mid] == target:
return mid, comparisons
elif items[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1, comparisons
data = [3, 8, 12, 17, 21, 25, 30, 36, 41, 47, 52, 58, 63, 70, 76]
print(binary_search(data, 63)) # (12, 4)
print(binary_search(data, 50)) # (-1, 4)Looking for 63, the middle positions checked are 7, 11, 13 and then 12. That is four comparisons where the linear search needed thirteen.
A trace table for binary search
A trace table follows each variable through the algorithm. This is the one the exam asks for, for target = 63:
| Pass | low | high | mid | items[mid] | Decision |
|---|---|---|---|---|---|
| 1 | 0 | 14 | 7 | 36 | 36 < 63, so low = 8 |
| 2 | 8 | 14 | 11 | 58 | 58 < 63, so low = 12 |
| 3 | 12 | 14 | 13 | 70 | 70 > 63, so high = 12 |
| 4 | 12 | 12 | 12 | 63 | Found at position 12 |
It does not crash and it does not warn you. It simply discards the half of the list the item happened to be in. Always say "the list must be sorted" when an exam asks for the condition.
How much faster?
Every comparison halves what is left, so doubling the size of the list adds only one more comparison.
for n in [16, 256, 4096, 65536, 1048576]:
steps = 0
size = n
while size > 0:
size = size // 2
steps += 1
print(n, "items need at most", steps, "comparisons")A million items in at most 21 comparisons. A linear search of the same list could need a million.
- Linear search looks at every item in turn from the start, so it can be slow.
- Binary search checks the middle and throws half the list away each time.
- 3 looks against 10. The longer the list, the bigger the gap, but binary search needs the list sorted.
Bubble sort and insertion sort
Sorting puts a list in order. The exam names three algorithms, and you must be able to describe each, trace it by hand and complete code for it.
Bubble sort
Compare each pair of neighbours and swap them if they are the wrong way round. After one pass the largest item has been carried to the end, like a bubble rising. Keep making passes until a whole pass has no swaps.
def bubble_sort(items):
items = items[:]
n = len(items)
passes = 0
swapped = True
while swapped:
swapped = False
for i in range(n - 1 - passes):
if items[i] > items[i + 1]:
temp = items[i]
items[i] = items[i + 1]
items[i + 1] = temp
swapped = True
passes += 1
print("after pass", passes, items)
return items
print(bubble_sort([5, 1, 4, 2, 8]))Run it and watch. After pass 1 the 8 is in place, after pass 2 the 5 is, and pass 3 makes no swaps, so the loop stops.
Two details earn marks:
- The swap needs a temporary variable.
items[i] = items[i + 1]followed byitems[i + 1] = items[i]loses a value. Python can swap in one line,items[i], items[i + 1] = items[i + 1], items[i], but the exam swap usestemp. - The
swappedflag lets the algorithm stop early. Without it, bubble sort always makes the full number of passes, even on a list that was sorted from the start. Then - 1 - passesshortens each pass because the end of the list is already final.
Insertion sort
Take the items one at a time and insert each into its correct place among the ones before it, which are already in order, shifting larger items one place right to make room. It is how most people sort a hand of cards.
def insertion_sort(items):
items = items[:]
for i in range(1, len(items)):
current = items[i]
j = i - 1
while j >= 0 and items[j] > current:
items[j + 1] = items[j]
j = j - 1
items[j + 1] = current
print("inserted", current, "->", items)
return items
print(insertion_sort([5, 1, 4, 2, 8]))Notice the 8 does no shifting at all. Insertion sort is quick on a list that is already nearly in order, which bubble sort is not.
- Compare two neighbours, and if they are the wrong way round, swap them.
- Work along the list. A big value is carried all the way to the end.
- After a pass the last item is in its final place, so the next pass can be shorter.
- Repeat until a whole pass makes no swaps.
- The sorted part grows from the left, one card at a time.
- Take the next card and slide it back past every larger card.
- Larger cards shift one place right to make room.
- When no cards are left, the whole list is sorted.
Merge sort
Merge sort uses divide and conquer. A list of one item is already sorted. So split the list in half again and again until every piece has a single item, then merge the pieces back together in order. Merging two sorted lists is easy: look at the front of each, and take the smaller.
def merge(left, right):
result = []
i = 0
j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i = i + 1
else:
result.append(right[j])
j = j + 1
result.extend(left[i:])
result.extend(right[j:])
return result
def merge_sort(items):
if len(items) <= 1:
return items
middle = len(items) // 2
left = merge_sort(items[:middle])
right = merge_sort(items[middle:])
return merge(left, right)
print(merge_sort([38, 27, 43, 3, 9, 82, 10]))merge_sort calls itself on each half. That technique is called recursion, and you are not required to write it at GCSE. What you must be able to do is show the splitting down to single items and the merging back up, as in the diagram.
Choosing between them
| Bubble sort | Insertion sort | Merge sort | |
|---|---|---|---|
| Idea | Swap neighbours | Insert each item into the sorted part | Split, then merge |
| Easy to write | Very | Yes | Harder |
| Big lists | Very slow | Slow | Fast |
| Nearly sorted list | Fast, with the flag | Fast | Same as any |
| Extra memory | None | None | Yes, for the merged copies |
Counting the work
Run a bubble sort on a reversed list, so it has the most to do, and count the comparisons as the list gets longer.
def bubble_comparisons(items):
items = items[:]
comparisons = 0
n = len(items)
for p in range(n - 1):
for i in range(n - 1 - p):
comparisons += 1
if items[i] > items[i + 1]:
items[i], items[i + 1] = items[i + 1], items[i]
return comparisons
for n in [10, 20, 40, 80]:
print(n, "items:", bubble_comparisons(list(range(n, 0, -1))), "comparisons")Doubling the list from 20 to 40 does not double the work, it quadruples it: 190 comparisons becomes 780. That is why bubble sort is fine for ten names and hopeless for a million. Merge sort's work grows far more slowly.
"Merge sort is faster" earns nothing. "Merge sort is faster on a large list because it does far fewer comparisons, since it halves the list each time, but it needs extra memory for the merged copies" earns three marks.
- Divide the list in half, again and again.
- Stop when every group is a single item. One item is already sorted.
- Merge groups back together, taking the smaller front item each time, until one sorted list is left.
Key terms you must be able to define
- Algorithm
- A step by step set of instructions for solving a problem.
- Linear search
- Checking each item of a list in turn, from the start, until the wanted item is found or the end is reached.
- Binary search
- Repeatedly comparing with the middle item of a sorted list and discarding the half that cannot contain the target.
- Bubble sort
- A sort that repeatedly compares neighbouring items and swaps them if they are in the wrong order, until a pass makes no swaps.
- Insertion sort
- A sort that takes each item in turn and inserts it into its correct place in the already sorted part of the list.
- Merge sort
- A divide and conquer sort that splits a list into single items and merges them back together in order.
- Divide and conquer
- Solving a problem by splitting it into smaller copies of itself, solving those and combining the answers.
- Trace table
- A table that records the values of variables at each step of an algorithm.
- Pass
- One complete run through the list by a sorting algorithm.
Flashcards
Say the definition out loud before you flip the card. Retrieving it from memory is what builds recall, reading it again is not.
Reach the top grade on this topic
Sorting and searching questions reward the exact order of events, so practise tracing until it is automatic.
- State that a binary search needs a sorted list, every time it is mentioned
- Show the middle position working: (low + high) DIV 2, and which half is discarded
- Write the state of the whole list after each pass of a bubble sort
- Say that a sort stops when a pass has no swaps, not when it has done n passes
- Compare algorithms using number of comparisons and memory, not the word "faster" alone
Mistakes that cost marks here
- Using a binary search on a list that is not in order.
- Swapping two items without a temporary variable, so one value is lost.
- Forgetting to carry on until a pass makes no swaps, and stopping after one pass.
- Calculating the middle with / and not DIV, which gives a decimal position.
- Saying a linear search is always slower, when for one search of an unsorted list it is the better choice.
Sorting is thought to have taken up a quarter of all computer time in the 1960s. Which algorithm to use mattered so much that Donald Knuth gave a whole volume of The Art of Computer Programming to it, and it is still the standard reference.
Knowledge check
[2, 4, 6, 8, 10, 12, 14] begins by comparing with which item?[5, 1, 4, 2, 8] after the first pass of a bubble sort?Exam-style questions
Five written questions in the style of the real paper. Write a full answer, then let the marker check it against the mark scheme.
| Position | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| Value | 3 | 9 | 14 | 17 | 22 | 28 | 35 |
- LFinds the middle item, which is 17
- LCompares 28 with 17 and discards the lower half because 28 is larger
- LTakes the middle of the remaining items, which is 28
- LCompares and finds 28, so the search is complete
The middle item of the seven is 17, at position 3. 28 is larger than 17, so the lower half and 17 itself are discarded, leaving 22, 28 and 35. The middle of those is 28, which matches, so the search stops and reports that 28 is at position 5.
- LDefines a function with two parameters, the list and the target
- LA loop that goes through the positions of the list
- LCompares each item with the target and returns the position when equal
- LReturns -1 after the loop, once every item has been checked
def linear_search(items, target):
for i in range(len(items)):
if items[i] == target:
return i
return -1
print(linear_search([4, 8, 15, 16], 15), linear_search([4, 8, 15, 16], 99))Each position is checked in turn and returned as soon as the item matches. The return of -1 is outside the loop, so it only happens when every item has been checked and none matched. The calls shown print 2 and -1.
- LAfter the first pass the list is [2, 6, 4, 9]
- LAfter the second pass the list is [2, 4, 6, 9]
- LA third pass is needed to confirm that no swaps are made
First pass: 6 and 2 swap to give 2, 6, 9, 4. 6 and 9 are in order. 9 and 4 swap, so the list is [2, 6, 4, 9]. Second pass: 2 and 6 are in order, 6 and 4 swap, giving [2, 4, 6, 9]. Another pass is needed, because the second pass made a swap, and it will confirm there are no swaps so the algorithm can stop.
- LA binary search can only be used on a sorted list
- LSorting the list takes more steps than one linear search would
- LThe search is only done once so there is no benefit from sorting
A binary search only works on a sorted list, so the names would have to be sorted first. Sorting 20 names takes more comparisons than simply checking all 20 once with a linear search. Because the search is only carried out once, there are no later searches to benefit from the sorted list, so the sorting would be wasted effort.
- LMerge sort needs far fewer comparisons on a large list
- LBecause it halves the list repeatedly, whereas a bubble sort compares neighbours over many passes
- LMerge sort needs extra memory to hold the merged lists
- LBubble sort sorts in place and is simpler to write, but is too slow for a million items
A merge sort is much better for a million names because it needs far fewer comparisons. It splits the list in half repeatedly and merges sorted halves, so the work grows slowly as the list grows, where a bubble sort makes many passes comparing neighbours and its work quadruples when the list is doubled. The cost of merge sort is that it needs extra memory for the lists it builds, whereas a bubble sort works in place and is simpler to write, but it would be far too slow for a million items.