Unit 27: Searching
Finding things, slowly and quickly.
Unit 27 of 31 in Python for kids. Its 5 lessons are One at a Time, Counting the Cost, Splitting in Half, Why Halving Wins and Search Master — below is everything each one explains, and a question or two from it to try.
Every sample on this page was run through real Python before it shipped, and prints exactly what it says it prints.
This unit opens with a fortnight’s trial of everything — no card needed — or with a family plan, bought in the iPhone app. The first two units of every track are free for ever. Try it in the app.
🔎 One at a Time
The obvious way
Linear search checks each item in turn until it finds what it wants. Simple, and it works on anything — sorted or not.
Python
def find(items, wanted):
for item in items:
if item == wanted:
return True
return False
print(find([4, 9, 2], 9))
print(find([4, 9, 2], 7))
It prints
True False
Where is it, not just whether
Usually you want the position. enumerate gives you the index as you go, and -1 is the usual way of saying "not here".
Python
def position(items, wanted):
for i, item in enumerate(items):
if item == wanted:
return i
return -1
print(position(["cat", "dog", "hen"], "dog"))
print(position(["cat", "dog", "hen"], "cow"))
It prints
1 -1
Python already has both
in and .index() are linear search, written for you. Write your own to understand it; use Python's in real code — except that .index() raises ValueError when nothing matches.
Python
pets = ["cat", "dog"]
print("dog" in pets)
print(pets.index("dog"))
try:
pets.index("cow")
except ValueError:
print("not in the list")
It prints
True 1 not in the list
Try it yourself
Why return -1 rather than 0 when nothing is found?
- 0 is faster
- 0 is a real position — the first one — so it would look like a match
- It has to be negative
- There is no reason
What does this print?
Python
def position(items, wanted):
for i, item in enumerate(items):
if item == wanted:
return i
return -1
print(position([5, 3, 5], 5))
Answer them in the app
⏱️ Counting the Cost
How much work is it really?
Count the comparisons instead of guessing. Finding something near the front is quick; finding the last item means checking every single one.
Python
def steps_to_find(items, wanted):
steps = 0
for item in items:
steps = steps + 1
if item == wanted:
return steps
return steps
numbers = list(range(1, 101))
print(steps_to_find(numbers, 1))
print(steps_to_find(numbers, 100))
It prints
1 100
The worst case is what counts
A list twice as long takes twice as long to search. Programmers call this O(n) — the work grows in step with the number of items.
Python
def steps(n):
return n
print(steps(10))
print(steps(100))
print(steps(1000))
It prints
10 100 1000
Searching things that are not numbers
Linear search works on anything you can compare. Here it looks through a list of dictionaries for the right name.
Python
people = [
{"name": "Ada", "age": 9},
{"name": "Sam", "age": 11},
]
def find_person(people, name):
for person in people:
if person["name"] == name:
return person["age"]
return None
print(find_person(people, "Sam"))
print(find_person(people, "Kim"))
It prints
11 None
Try it yourself
A linear search of 500 names takes at most how many comparisons?
- 1
- About 9
- 250
- 500
Answer it in the app
✂️ Splitting in Half
The way you look up a name in a phone book
You do not start at page one. You open the middle, see which half the name is in, and throw the other half away. That is binary search — the same halving a binary search tree does, on a plain list.
Python
numbers = [1, 3, 5, 7, 9, 11]
low = 0
high = len(numbers) - 1
middle = (low + high) // 2
print(middle)
print(numbers[middle])
It prints
2 5
The whole algorithm
Keep two markers for the part still worth looking at. Check the middle; if it is too small move low up, if too big move high down. Stop when they cross.
Python
def binary_search(items, wanted):
low = 0
high = len(items) - 1
while low <= high:
middle = (low + high) // 2
if items[middle] == wanted:
return middle
if items[middle] < wanted:
low = middle + 1
else:
high = middle - 1
return -1
numbers = [1, 3, 5, 7, 9]
print(binary_search(numbers, 9))
print(binary_search(numbers, 4))
It prints
4 -1
It only works on sorted data
This is the catch. Binary search decides which half to keep by comparing — and on an unsorted list that decision is meaningless, so it will happily miss something that is right there.
Python
def binary_search(items, wanted):
low = 0
high = len(items) - 1
while low <= high:
middle = (low + high) // 2
if items[middle] == wanted:
return middle
if items[middle] < wanted:
low = middle + 1
else:
high = middle - 1
return -1
print(binary_search([9, 1, 5, 3], 9))
print(binary_search(sorted([9, 1, 5, 3]), 9))
It prints
-1 3
Try it yourself
Why must low become middle + 1 rather than middle?
- To make it faster
- The middle has already been checked, and leaving it in would loop for ever
- It does not matter
- To avoid going past the end
What does this print?
Python
items = [2, 4, 6, 8]
low = 0
high = 3
middle = (low + high) // 2
print(middle, items[middle])
Answer them in the app
📉 Why Halving Wins
Watch the difference
Same list, same target, both searches counted. This is the whole unit in one output.
Python
numbers = list(range(1, 101))
def linear_steps(items, wanted):
steps = 0
for item in items:
steps = steps + 1
if item == wanted:
return steps
return steps
def binary_steps(items, wanted):
steps = 0
low = 0
high = len(items) - 1
while low <= high:
steps = steps + 1
middle = (low + high) // 2
if items[middle] == wanted:
return steps
if items[middle] < wanted:
low = middle + 1
else:
high = middle - 1
return steps
print(linear_steps(numbers, 100))
print(binary_steps(numbers, 100))
It prints
100 7
Doubling the list adds one step
Halving means the list has to *double* before the search needs one more step. Programmers call this O(log n) — and it is why a million items still only take about twenty.
Python
sizes = [10, 100, 1000, 1000000]
for size in sizes:
steps = 0
remaining = size
while remaining > 0:
steps = steps + 1
remaining = remaining // 2
print(size, steps)
It prints
10 4 100 7 1000 10 1000000 20
The recursive version
Binary search says "search this smaller part", which is a recursive idea. The base case is running out of list.
Python
def search(items, wanted, low, high):
if low > high:
return -1
middle = (low + high) // 2
if items[middle] == wanted:
return middle
if items[middle] < wanted:
return search(items, wanted, middle + 1, high)
return search(items, wanted, low, middle - 1)
numbers = [1, 3, 5, 7, 9]
print(search(numbers, 7, 0, len(numbers) - 1))
It prints
3
Python has it built in
The bisect module does binary search properly. bisect_left gives the position where a value is, or where it *would* go — handy for keeping a list sorted as you add to it.
Python
from bisect import bisect_left, insort
numbers = [1, 3, 5, 7]
print(bisect_left(numbers, 5))
print(bisect_left(numbers, 4))
insort(numbers, 4)
print(numbers)
It prints
2 2 [1, 3, 4, 5, 7]
Try it yourself
You must search a list once, and it is not sorted. What is usually best?
- Sort it first, then binary search
- Linear search — sorting costs more than the one search saves
- Binary search on the unsorted list
- Neither will work
Answer it in the app
🏆 Search Master
Try it yourself
What does this print?
Python
items = [1, 2, 3, 4, 5, 6, 7]
low = 0
high = 6
middle = (low + high) // 2
low = middle + 1
middle = (low + high) // 2
print(items[middle])
Answer it in the app