Binary search and the bisect module¶
~20 min ยท prerequisite: Prefix sums and difference arrays
What you will be able to do¶
- Find a position in a sorted list with the
bisectmodule, and choose betweenbisect_leftandbisect_right. - Search for the smallest or largest answer that satisfies a yes-or-no test.
- Show that a binary search takes about
log2of the range size steps.
The idea¶
Binary search finds something in a sorted range by checking the middle and throwing away half of the range. Suppose you are looking for a word in a printed dictionary. You open near the middle, see whether your word comes before or after that page, and continue in one half. Each check halves the pages still in play.
The same idea works on a list. For a sorted list, the question "is the value at mid too small?" has a clear answer, and that answer tells you which half to keep. Python's standard library already has this search in the bisect module. The function bisect_left(values, x) returns the first index where x could be inserted while keeping the list sorted. bisect_right(values, x) returns the index after the last position that holds x. The two differ only when x is already in the list.
The second use of binary search is on an answer. Suppose a yes-or-no test ok(x) is monotone, which means it changes once: it is False for small values and True from some point on. Then the smallest x with ok(x) true can be found by the same halving.
Why are the steps few? Let d be the distance hi - lo between the two ends of the range. After one check, the new distance is at most d // 2. Starting from d, after s checks the distance is at most d / 2^s. The search ends when the distance reaches zero, which happens once 2^s is larger than d. So at most log2(d) + 1 checks are needed. For a range of a billion values, that is about 31 checks.
Worked example¶
The function below finds the smallest integer k with k * k >= n. The test k * k >= n is monotone, so binary search applies.
def smallest_root_ceiling(n):
lo, hi = 0, n
while lo < hi:
mid = (lo + hi) // 2
if mid * mid >= n:
hi = mid
else:
lo = mid + 1
return lo
assert smallest_root_ceiling(50) == 8
assert smallest_root_ceiling(49) == 7
Line by line, this is what Python does for n = 50:
- The answer lies in
lothroughhi, starting with0and50. Both bounds are included. - The loop runs while the range has more than one value.
mid = (lo + hi) // 2is the middle index, rounded down.- If
mid * mid >= n, thenmidis large enough, and the answer ismidor something smaller. Sohi = midkeepsmidin the range. - Otherwise
midis too small, and the answer is at leastmid + 1, solo = mid + 1. - When
loequalshi, the loop stops andlois the answer. Forn = 50it is8, because7 * 7 = 49is too small and8 * 8 = 64is enough.
Try it¶
The first block checks bisect results on a small list. The second finds the largest square root with a search that rounds the middle up.
Common mistakes¶
-
Mixing up
bisect_leftandbisect_right. Withvalues = [1, 2, 2, 2, 3], the right index for2is4, not1:Use
bisect_leftfor the first position of a value, and subtract the two results to count copies. -
Reading past the end of the list. When the search uses
hi = len(values)as an index, the middle can point one place past the end:Check
mid < len(values)before readingvalues[mid], or search on a range that ends atlen(values) - 1. -
Looping forever with
while lo <= hi. If you use<=but still writehi = mid, the range can stop shrinking whenloequalshi. No error appears; the judge reportsTime limit exceeded. Usewhile lo < hiwith the update rules from the worked example. -
Applying the search to a test that is not monotone. If
ok(x)switches back and forth, the search can return a value that is not the smallest. No error appears, so test the rule on a few small inputs against a brute-force loop.
Practice¶
- Easy: check that
bisect.bisect_left([10, 20, 30], 25)is2. - Easy: using
bisect, count how many times4appears in[1, 4, 4, 4, 7]. The answer is3. - Medium: write
smallest_cube_root(n)that returns the smallestx >= 0withx ** 3 >= n, using binary search. Checksmallest_cube_root(1000)is10andsmallest_cube_root(0)is0. - Medium: write
largest_root(n)as in the third runnable block, and check thatlargest_root(99)is9. - Stretch: given a sorted list
aand an integerq >= 0, count pairsi < jwitha[j] - a[i] <= q. Usebisect_rightfor eachi, and check the answer on[1, 3, 4, 8]withq = 3, which is3.
Solution
- ```python import bisect
assert bisect.bisect_left([10, 20, 30], 25) == 2 ```
- ```python import bisect
values = [1, 4, 4, 4, 7] count = bisect.bisect_right(values, 4) - bisect.bisect_left(values, 4) assert count == 3 ```
- ```python def smallest_cube_root(n): lo, hi = 0, n while lo < hi: mid = (lo + hi) // 2 if mid ** 3 >= n: hi = mid else: lo = mid + 1 return lo
assert smallest_cube_root(1000) == 10 assert smallest_cube_root(0) == 0 ```
For n = 0, the range is 0 through 0, so the loop does not run and the answer is 0.
- ```python def largest_root(n): lo, hi = 0, n while lo < hi: mid = (lo + hi + 1) // 2 if mid * mid <= n: lo = mid else: hi = mid - 1 return lo
assert largest_root(99) == 9 ```
The middle is rounded up here. Without that, lo = mid could repeat forever when hi = lo + 1.
- ```python import bisect
def count_close_pairs(a, q): total = 0 for i in range(len(a)): reach = bisect.bisect_right(a, a[i] + q) total += reach - (i + 1) return total
assert count_close_pairs([1, 3, 4, 8], 3) == 3 ```
For each i, bisect_right gives the number of values at most a[i] + q. Subtracting i + 1 removes the values at or before i, so only later positions are counted.
Recap¶
bisect_leftgives the first position for a value, andbisect_rightgives the position after the last one.- Binary search on a monotone test finds the smallest or largest answer in about
log2of the range size steps. - Check the update rules when the range can shrink to two values, and make sure the middle index stays inside the list.