Intermediate: core algorithms · Sorting and searching · topic 29 of 61 · 40 min
Binary search
Explanation
The idea
You met recursive binary search in Module 5. Here is the loop version, which uses O(1) space, and its variants. The list must be sorted. Look at the middle item. If it is the target, stop. If it is smaller than the target, the target can only be to the right, so discard the left half; otherwise discard the right half. Each step halves the range, so a million items need at most 20 steps (2²⁰ is about a million).
The exact loop
def binary_search(nums, target):
lo, hi = 0, len(nums) - 1 # the search range is nums[lo..hi], both included
while lo <= hi: # <= : a one-item range must still be checked
mid = (lo + hi) // 2
if nums[mid] == target:
return mid
if nums[mid] < target:
lo = mid + 1 # mid is too small: drop it and everything left
else:
hi = mid - 1 # mid is too big: drop it and everything right
return -1
print(binary_search([1, 3, 5, 7, 9, 11], 7)) # Output: 3
print(binary_search([1, 3, 5, 7, 9, 11], 4)) # Output: -1The invariant (a fact that stays true every round) is: if the target is anywhere, it is inside nums[lo..hi]. Every line keeps that true, and the range shrinks by at least one each round, so the loop ends. When lo passes hi, the range is empty and the target is missing.
Off-by-one mistakes
- while lo < hi with inclusive bounds: the last remaining item is never checked.
- lo = mid instead of lo = mid + 1: when hi = lo + 1, mid equals lo and the loop runs forever.
- hi = len(nums) with inclusive bounds: nums[mid] can go past the end.
- In Java or C++, (lo + hi) / 2 can overflow a 32-bit int for huge arrays; write lo + (hi − lo) / 2. Python integers do not overflow, but interviewers like this point.
Lower bound and upper bound
Often you need a position, not just "found or not". The lower bound of x is the first index whose value is ≥ x; the upper bound is the first index whose value is > x. Both are also the places to insert x and keep the list sorted. These use a half-open range [lo, hi), where hi is NOT included, and the loop runs while lo < hi. The same template finds "the first index where a condition becomes true", which is the heart of Topic 5.
def lower_bound(nums, x):
lo, hi = 0, len(nums) # half-open: nums[lo..hi-1]
while lo < hi:
mid = (lo + hi) // 2
if nums[mid] < x:
lo = mid + 1
else:
hi = mid # mid might be the answer: keep it
return lo
nums = [1, 2, 4, 4, 4, 7]
print(lower_bound(nums, 4), lower_bound(nums, 5), lower_bound(nums, 9)) # Output: 2 5 6Python's bisect module does exactly this in C: bisect.bisect_left(nums, x) is the lower bound and bisect.bisect_right(nums, x) is the upper bound. bisect.insort(nums, x) inserts x in order (the search is O(log n), but inserting into the list shifts items, O(n)). Since Python 3.10 these functions also accept key=.
from bisect import bisect_left, bisect_right
nums = [1, 2, 4, 4, 4, 7]
print(bisect_left(nums, 4), bisect_right(nums, 4)) # Output: 2 5
print(bisect_right(nums, 4) - bisect_left(nums, 4)) # Output: 3 (how many 4s)First and last position of a value
With duplicates, the first position of x is lower_bound(x) if that index holds x; the last position is upper_bound(x) − 1. Two binary searches, O(log n) total. Scanning left and right from any match would be O(n) when the whole list is x.
Search in a rotated sorted array
A sorted list of distinct values is rotated at some unknown point, for example [4, 5, 6, 7, 0, 1, 2]. It is no longer sorted, but at any mid, at least one of the halves nums[lo..mid] and nums[mid..hi] IS sorted. Check which: if nums[lo] ≤ nums[mid], the left half is sorted. If the target lies within that sorted half's range, search there; otherwise search the other half. Still O(log n).
Common mistakes
- Running binary search on unsorted data. Fix: sort first (O(n log n)) or use a set or dict for single lookups.
- Mixing the inclusive template (lo <= hi, hi = mid − 1) with the half-open one (lo < hi, hi = mid). Fix: pick one template and keep all four lines consistent.
- Returning any match when the problem asks for the first one. Fix: use the lower bound, and keep searching left after a match.
- Using nums.index(x) or "x in nums" inside a loop, which is O(n) each. Fix: bisect for sorted data.
- Forgetting the empty list. Fix: with hi = len(nums) − 1 = −1 the loop simply does not run; check that your code returns the right value.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Binary search · 1/8
This lesson teaches binary search properly: the idea, the exact loop, and the variants interviewers love.
Prefer a video by a teacher? Find free videos on “binary search lower bound upper bound bisect python rotated sorted array explained” on YouTube ↗
In real life: Finding a PNR in a sorted booking log
A railway booking system writes bookings to a log in order of booking ID (a PNR-like number that only grows). To find a booking, or all bookings in an ID range, the system does not scan millions of lines; it binary searches the sorted IDs. Database indexes (B-trees) use the same "keep the data sorted, halve the search" idea at a larger scale, which Module 10 of the SQL course covers.
from bisect import bisect_left, bisect_right
pnrs = [2104, 2110, 2111, 2150, 2188, 2190, 2205]
lo = bisect_left(pnrs, 2110)
hi = bisect_right(pnrs, 2190)
print(pnrs[lo:hi])Practice (0/4 done)
Write binary_search(nums, target) with a LOOP (not recursion). nums is sorted in ascending order with distinct values. Return the index of target, or -1 if it is missing. O(log n) time, O(1) extra space; do not use nums.index or the in operator. Example: binary_search([1, 3, 5, 7, 9, 11], 7) returns 3.
Write first_and_last(nums, target). nums is sorted in non-decreasing order and may contain duplicates. Return [first, last]: the first and last index where target appears, or [-1, -1] if it does not appear. Use two binary searches (lower bound and upper bound) for O(log n) time; you may write them yourself or use the bisect module. Example: first_and_last([5, 7, 7, 8, 8, 10], 8) returns [3, 4].
A list of DISTINCT integers was sorted in ascending order and then rotated at an unknown point, for example [0, 1, 2, 4, 5, 6, 7] might become [4, 5, 6, 7, 0, 1, 2]. Write search_rotated(nums, target) that returns the index of target or -1, in O(log n) time: at each step, one of the two halves around mid is sorted; decide whether target lies inside it. Example: search_rotated([4, 5, 6, 7, 0, 1, 2], 0) returns 4.
Write insert_position(nums, target). nums is sorted in non-decreasing order (duplicates allowed). Return the SMALLEST index at which target could be inserted so that nums stays sorted: the same answer as bisect.bisect_left. Write the binary search yourself in O(log n). Example: insert_position([1, 3, 5, 6], 2) returns 1.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Binary search on the answerSorting and searching · same module
- Merge sort and quick sortSorting and searching · same module
- Big-O and searchingPython Programming · Algorithms, data structures and testing
- Two-dimensional arrays and array algorithmsJava Programming (Core to Spring Boot) · Arrays and strings
- Testing, edge cases and debugging under pressureInterview strategy and mock problems
- STL algorithms: sort, find, count, accumulate and binary searchC++ Programming (modern C++ and DSA) · The Standard Template Library (STL)
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
At most how many steps does binary search need on 1,000,000 sorted items?
Answer: About 20
Each step halves the range; 2²⁰ is just over a million.
With an inclusive range (hi = len(nums) − 1), why must the loop be while lo <= hi?
Answer: So a range with one item is still checked
When lo == hi one item remains; while lo < hi would stop without checking it.
nums = [1, 2, 4, 4, 4, 7]. What is bisect_right(nums, 4)?
Answer: 5
bisect_right is the first index with a value greater than 4, which is index 5 (the 7).
Why can lo = mid (instead of mid + 1) loop forever in the inclusive template?
Answer: When hi = lo + 1, mid equals lo, so the range never shrinks
(lo + hi) // 2 rounds down, so with two items mid = lo, and lo = mid changes nothing.
In a rotated sorted array, nums[lo] <= nums[mid]. What do you know?
Answer: The left half nums[lo..mid] is sorted
If the start of the range is not bigger than mid, there is no rotation point between them.
About how many steps does binary search need for 1,000,000 sorted items?Related
Answer: About 20
Halving 1,000,000 about 20 times gets down to 1, because 2 to the power 20 is about a million.
From Big-O and searching
Rotating a list of 5 items right by 12 is the same as rotating by…Related
Answer: 2
12 % 5 = 2. Every full 5 rotations bring the list back to where it started.
Why pass lo and hi instead of slicing nums[mid + 1:] in recursive binary search?Related
Answer: Slicing copies the half, adding O(n) work and memory
A slice copies about n/2 items, so the search would cost O(n) instead of O(log n).
From Divide and conquer
Why write mid = lo + (hi - lo) / 2?Related
Answer: It avoids int overflow of lo + hi on huge arrays
lo + hi can exceed Integer.MAX_VALUE.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 1 more topic in “Sorting and searching” before its exam: Binary search on the answer.
- Module 6 exam: Sorting and searching · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Trees and binary search trees. 61 topics left in the course before the final exam.
← Previous: Sorting in real code: keys, comparators, counting sort