Binary Search & Library Search
A Tunebox library holds 50,000 titles, sorted A→Z. Checking them one by one means up to 50,000 comparisons for a title that isn't there. Binary search opens the library in the middle, sees which half the title must be in, and throws the other half away — 16 looks, at most, for all 50,000.
Halving the search space
Linear search works on anything. Binary search needs one thing in return: the data must be sorted, so a single comparison tells you which side to keep.
Each look halves what's left, so the number of looks is how many times you can halve n before nothing remains — about log₂ n:
n, looks = 1_000_000, 0
while n > 0:
n //= 2
looks += 1
print(looks)The invariant that keeps it honest
Binary search is short, and famously easy to get subtly wrong. The cure is to
say out loud what lo and hi mean, and keep it true every step:
Invariant: if
titleis anywhere in the list, it is at an index inlo..hi(both ends included).
- Start:
lo = 0,hi = len - 1— the whole list, so it's true. titles[mid] < title→ the title can't be atmidor left of it:lo = mid + 1.titles[mid] > title→ it can't be atmidor right of it:hi = mid - 1.- When
lo > hithe range is empty — and by the invariant, the title isn't there.
Watch lo and hi close in: the range is 0..4, then 3..4, then mid lands on
"Dawn".
Off-by-one: the classic bugs
Almost every broken binary search breaks the invariant at one of three spots:
| bug | what happens |
|---|---|
while lo < hi with an inclusive hi |
the last candidate (lo == hi) is never checked |
hi = mid with an inclusive hi |
mid stays in range forever — an infinite loop |
lo = mid instead of mid + 1 |
when hi == lo + 1, mid == lo and nothing moves |
Here is the first one. It looks right, and fails on the last title:
This should print 2, the index of "Cove". It prints -1 — the search gives up while one title is still unchecked.
❓ Cross-question — "Why
(lo + hi) // 2and not/ 2?"/always gives a float, andtitles[2.0]raisesTypeError.//floors to an int.
Coming from Java/JS: you may have learned
lo + (hi - lo) / 2to dodge integer overflow. Python ints never overflow, so(lo + hi) // 2is safe. In JS rememberMath.floor—(lo + hi) / 2is a float there too.
Lower and upper bound
"Is it there?" is only one question. With duplicates — say, play counts — the useful questions are where does 20 start? and where does it end?
- lower bound: the first index whose value is
>= x - upper bound: the first index whose value is
> x
These use a half-open range [lo, hi) — hi = len(a), because the answer
may be "past the end":
Here hi = mid is correct, not a bug — the range is half-open, so mid stays a
candidate and the loop still shrinks, because mid < hi always.
The bisect module
Python ships both bounds, written in C:
from bisect import bisect_left
titles = ["Aria", "Cove", "Echo"]
print(bisect_left(titles, "Dawn"))❓ Cross-question — "Can I bisect a list of track dicts by title?" Since Python 3.10, yes:
bisect_left(tracks, "Dawn", key=lambda t: t["title"]). The list must already be sorted by that same key.
Binary search on the answer
The same halving works when there is no list at all — only a range of possible answers and a yes/no test that flips once. Tunebox shows album covers in a square grid: with 50 covers, what's the biggest full square?
"Does mid fit?" is yes, yes, yes, … no, no as mid grows — that single flip
is all binary search needs. When you move lo = mid (keeping mid), round mid
up: with lo = 3, hi = 4, rounding down picks 3 again and loops forever.
| operation | time |
|---|---|
| linear search | O(n) |
| binary search, lower / upper bound | O(log n) |
bisect_left, bisect_right |
O(log n) |
insort |
O(n) — finding the spot is O(log n), shifting the list is O(n) |
| search on an answer range of size R | O(log R × cost of one yes/no test) |
Idioms & real-world patterns
Count and range queries with two bisects
How many tracks have between 15 and 30 plays? Two bounds, one subtraction:
Bucket a value into ranges
bisect on a list of cut-offs turns a number into a band, with no if chain:
⚡ Advanced — "first bad version"
Binary search finds the first True in any monotone yes/no sequence — the first
build where a bug appeared, the first day a track crossed a million plays. Make
the question a function and search the indexes:
Every binary search in this lesson is this one function with a different test.
🎯 Your turn
Write find_track(sorted_titles, title). sorted_titles is sorted A→Z; return
the index of title using binary search, or -1 if it isn't there:
find_track(["a", "c", "e", "g"], "e")→2find_track(["a", "c", "e"], "b")→-1
Hint — keep lo and hi inclusive, loop while lo <= hi, and move past
mid with mid + 1 / mid - 1.
Then press ▶ Run, tap the Live App try chips to call it with different inputs, and hit ✓ Check. Green = this piece of Tunebox is built. ✅
