BSRC-3

First and last occurrence

Binary searchLesson 3 of 615 min

The search from the first two lessons finds a copy of the target. But often we need a specific copy: the first one, the last one, or the number of copies. This lesson shows a different way to think about binary search, which the rest of this station uses.

When the target appears more than once

Take t = 7. The basic search returns whichever 7 it happens to land on. On the 16-value list in the figure below, it lands on index 3, the first copy, but only by luck. On [7, 7, 7, 7, 7] it returns index 2. Which copy you get depends on the length of the list, not on what you asked for. Code that depends on this will break.

One fix is to find any 7 and then step left until the value changes. But that costs one read for each copy. If the list has a million 7s, it costs a million reads, and the search is no longer O(log n). We want to find the first 7 in O(log n) reads, however many copies there are.

Search for a boundary, not a value

Instead of asking “is this the target?”, ask a yes-or-no question about each index:

Because a is sorted, once a[i] >= t is true, it stays true for every later index. So the answers are a block of F (false) followed by a block of T (true). There is exactly one point where F changes to T, and binary search can find it. If the value at that point equals t, it is the first copy. If not, t is not in the list.

To find the last copy, ask a[i] > t instead. Its first T is the first value bigger than t. So the last copy of t, if there is one, is just before it. The number of copies is the distance between these two points.

FIG BSRC-3.1Finding the first 7 by finding a boundary
Find
2
4
4
7
7
7
7
9
12
12
15
18
18
18
21
25
reads 0window 16
1/10

For each index, ask: is a[i] >= 7? The answers go F F … F T T … T. Start with lo = 0 and hi = 16. In a half-open window, hi is one past the end.

Step 1 of 10

Look at the row of F and T labels under the list. A label changes from ? to F or T only when the search has proved it. The window is exactly the part still marked ?.

The half-open window

This search is simplest with a half-open window, written [lo, hi). Here lo is the first index we have not settled yet, and hi is the first index we know is T. At the start hi = n, which is one past the end of the list. It stands for “no T found yet”.

first_true.pyPython
def first_at_least(a, t):
    """Smallest i with a[i] >= t, or len(a) if there is none."""
    lo, hi = 0, len(a)
    while lo < hi:
        mid = (lo + hi) // 2
        if a[mid] >= t:
            hi = mid          # mid may be the first T: keep it
        else:
            lo = mid + 1      # mid is F: drop it
    return lo                 # lo == hi: the boundary

At the start, both parts of this statement are about empty ranges, so they are true. If mid is T, every index from mid onwards is T, so we set hi = mid. If mid is F, every index up to mid is F, so we set lo = mid + 1. Since lo ≤ mid < hi, both updates make the window smaller. When lo == hi, nothing is unknown, and lo is the boundary.

Notice the difference between the two updates. hi = mid keeps mid, because it might be the answer. lo = mid + 1 removes mid, because an F is never the answer. Lesson 6 shows what goes wrong if you mix these up.

bisect_left and bisect_right

This loop is almost exactly what Python’s bisect module does:

The bisect functions and the questions they search
CallReturns the first index whereUse it for
bisect_left(a, t)a[i] >= tthe first copy of t, or where to insert t before equal values
bisect_right(a, t)a[i] > tone past the last copy, or where to insert t after equal values
bisect_right − bisect_left(a count, not an index)the number of copies of t, in O(log n)

Both functions accept lo and hi arguments to search only part of a list. Since Python 3.10, they also accept a key function, so you can search a list of records sorted by one field. insort inserts a value and keeps the list sorted. Finding the place takes O(log n), but moving the later items along takes O(n). So insort does not make a list into a fast sorted container.

BSRC-3 counts as completed once you step through the figure to the last step.