Python Arena
Step 4 of 4

Binary search

To find out whether a value is in a list, in (or a loop) looks at the items one by one from the start. That's a linear search. In a list of a million items, it may have to look at all million.

If the list is sorted, there's a much faster way. Think of guessing a number between 1 and 100 when you're told "higher" or "lower" after each guess. You start with 50, and whatever the answer, half of the numbers are out of the game. Binary search does the same with a sorted list:

  1. Look at the item in the middle of the search area.
  2. If it's the one you're looking for, you're done.
  3. If it's smaller than what you're looking for, the target can only be in the right half. Search there.
  4. If it's greater, search the left half.
  5. If the search area is empty, the value isn't in the list.

Steps 3 and 4 are the same search on a smaller area, so recursion fits well. The function gets the list, the target and the edges of the search area, low and high, as indexes. It returns the target's index, or -1 when the target isn't there. A print in each call shows how the area shrinks:

def find(numbers, target, low, high):
    if low > high:
        return -1
    middle = (low + high) // 2
    print(f"searching {low}..{high}, middle item {numbers[middle]}")
    if numbers[middle] == target:
        return middle
    if numbers[middle] < target:
        return find(numbers, target, middle + 1, high)
    return find(numbers, target, low, middle - 1)


numbers = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
print(find(numbers, 23, 0, len(numbers) - 1))
print(find(numbers, 10, 0, len(numbers) - 1))
Output
searching 0..9, middle item 16
searching 5..9, middle item 56
searching 5..6, middle item 23
5
searching 0..9, middle item 16
searching 0..3, middle item 5
searching 2..3, middle item 8
searching 3..3, middle item 12
-1

The first call searches the whole list, from index 0 to len(numbers) - 1. (low + high) // 2 is the index in the middle, rounded down. When the middle item is too small, the new area starts at middle + 1: the middle item has been checked already, so it's left out. In the second search, the last call gets low 3 and high 2. The area is empty, and that's the base case that returns -1.

Why it's fast

Every call halves the search area. A list of 10 items needs at most 4 looks, and a list of a million items needs at most 20, because a million can be halved only about 20 times before a single item is left. A linear search might need a million looks.

A wrapper, and a warning

So that the people who use the search don't need to think about low and high, you can wrap it in a function of its own. Here contains starts the search on the whole list and turns the index into True or False. Binary search works on anything sorted, such as words in alphabetical order, since < compares strings alphabetically:

def find(numbers, target, low, high):
    if low > high:
        return -1
    middle = (low + high) // 2
    if numbers[middle] == target:
        return middle
    if numbers[middle] < target:
        return find(numbers, target, middle + 1, high)
    return find(numbers, target, low, middle - 1)


def contains(items, target):
    return find(items, target, 0, len(items) - 1) != -1


print(contains([2, 5, 8, 12, 16], 12))
print(contains(["ant", "bee", "cat", "dog"], "cat"))
print(contains([9, 2, 7, 4, 1], 9))
Output
True
True
False

The last answer is wrong: 9 is in the list. But the list isn't sorted, and binary search trusts the order. It looked at 7 first, decided that 9 could only be on the right, and never saw the 9 at the start. Binary search only works on a sorted list.

Your task

Challenge 1 of 3

Fill in the blanks with the new edges of the search area: where the right half starts and where the left half ends. The program should print:

5
1
-1

Challenge 2

This binary search has a bug. A search for 4 in [1, 3, 5, 7] never ends, and Python stops it with a RecursionError. Find out why and fix it, so the function returns the target's index, or -1 when the target isn't in the list.

For example, this call:

print(find([1, 3, 5, 7], 4, 0, 3))

should print:

-1

Challenge 3

Write a recursive function find_name(names, name, low, high) that searches a list of names in alphabetical order with binary search. It returns the index of name in the list, or -1 if the name isn't there. Don't use loops, in or index. A search that steps through the names one at a time won't do either: the tests also search a list of 3000 names, and a call for each name ends in a RecursionError.

For example, this program:

names = ["Ada", "Ben", "Cleo", "Dan", "Eva", "Finn", "Gus"]
print(find_name(names, "Eva", 0, len(names) - 1))
print(find_name(names, "Bob", 0, len(names) - 1))

prints:

4
-1