DSA course menu
Binary Search Explained: Iterative, Recursive and Boundary Searches
Learn binary search in Python: how halving gives O(log n), the iterative template, finding the first and last position of a value, the bisect module, and binary search on the answer.
On this page
Binary search finds a value in a sorted list by repeatedly halving the part that could contain it. Each comparison throws away half of what is left, so a million items need only about 20 comparisons: O(log n).
It is how you look up a word in a paper dictionary: open the middle, see whether your word comes before or after, and repeat in that half.
The iterative version#
Keep a range [low, high] that must contain the target if it is present:
def binary_search(items, target):
low, high = 0, len(items) - 1
steps = 0
while low <= high:
steps += 1
mid = (low + high) // 2
if items[mid] == target:
return mid, steps
if items[mid] < target:
low = mid + 1 # target is in the right half
else:
high = mid - 1 # target is in the left half
return -1, steps
numbers = list(range(0, 2_000_000, 2)) # 1,000,000 even numbers
print(binary_search(numbers, 1_234_568))
print(binary_search(numbers, 7)) # odd: not present
print(binary_search([3, 8, 15, 23, 42], 23))(617284, 17)
(-1, 20)
(3, 2)A million items, found in at most 20 steps. A linear scan could need a million.
Tracing a search#
Printing the range on each step shows the halving:
def trace(items, target):
low, high = 0, len(items) - 1
while low <= high:
mid = (low + high) // 2
print(f"low={low} high={high} mid={mid} value={items[mid]}")
if items[mid] == target:
return mid
if items[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1
print(trace([2, 5, 8, 12, 16, 23, 38, 56, 72, 91], 23))low=0 high=9 mid=4 value=16
low=5 high=9 mid=7 value=56
low=5 high=6 mid=5 value=23
5Finding boundaries: first and last occurrence#
When values repeat, plain binary search returns some matching index. To find the first one, keep searching left after a match:
def first_index(items, target):
low, high, found = 0, len(items) - 1, -1
while low <= high:
mid = (low + high) // 2
if items[mid] >= target:
if items[mid] == target:
found = mid
high = mid - 1 # keep looking left
else:
low = mid + 1
return found
def last_index(items, target):
low, high, found = 0, len(items) - 1, -1
while low <= high:
mid = (low + high) // 2
if items[mid] <= target:
if items[mid] == target:
found = mid
low = mid + 1 # keep looking right
else:
high = mid - 1
return found
data = [1, 2, 2, 2, 3, 5, 5, 8]
print(first_index(data, 2), last_index(data, 2))
print(first_index(data, 5), last_index(data, 5))
print(first_index(data, 4), last_index(data, 4))1 3
5 6
-1 -1The number of times a value appears is last - first + 1, found in O(log n).
The bisect module#
Python's standard library already has these boundary searches. bisect_left returns the first position where the value could be inserted keeping the list sorted; bisect_right returns the position after any equal values:
from bisect import bisect_left, bisect_right, insort
data = [1, 2, 2, 2, 3, 5, 5, 8]
print(bisect_left(data, 2), bisect_right(data, 2))
print(bisect_left(data, 4)) # where 4 would go
insort(data, 4) # insert keeping it sorted
print(data)1 4
5
[1, 2, 2, 2, 3, 4, 5, 5, 8]insort finds the position in O(log n), but inserting into a list still shifts items, so it is O(n) overall.
Binary search on the answer#
Binary search works on any yes/no question that flips once: false, false, …, true, true. You do not even need a list.
Workers must ship packages (in order) within days days. What is the smallest daily capacity that works? If a capacity works, any larger one also works, so search the capacity:
def days_needed(weights, capacity):
days, load = 1, 0
for w in weights:
if load + w > capacity:
days += 1
load = 0
load += w
return days
def min_capacity(weights, days):
low, high = max(weights), sum(weights) # the answer is in this range
while low < high:
mid = (low + high) // 2
if days_needed(weights, mid) <= days:
high = mid # mid works: try smaller
else:
low = mid + 1 # too small
return low
packages = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(min_capacity(packages, 5))
print(days_needed(packages, 15), days_needed(packages, 14))15
5 6With capacity 15 the packages fit in 5 days; 14 is not enough. The search tries about log₂(55 − 10) ≈ 6 capacities, each checked in O(n).
Common mistakes#
Practice#
Practice exercise
Write int_sqrt(n) that returns the largest whole number whose square is at most n, using binary search (no math.sqrt or ** 0.5).
Show hint
Search the range 0 to n. If mid * mid <= n, mid is a candidate and the answer may be bigger; otherwise it is smaller.
Show solution
def int_sqrt(n):
low, high, answer = 0, n, 0
while low <= high:
mid = (low + high) // 2
if mid * mid <= n:
answer = mid
low = mid + 1
else:
high = mid - 1
return answer
print([int_sqrt(n) for n in [0, 1, 8, 9, 15, 16, 1_000_000]])[0, 1, 2, 3, 3, 4, 1000]Key takeaway#
Next: how lists get sorted in the first place, in Bubble, Selection and Insertion Sort.
Finished this lesson?