Builds on Lower Bound, which you haven’t finished yet.
Classic Binary Search
target, and then checks whether that value is equal to target.Binary Search (LC 704)(opens leetcode.com)
nums is an integer array sorted in ascending order, and target is an integer; find where target sits in nums. Return that index if target is present, or -1 if it isn't.
Your algorithm must run in O(log n) time.
Exact Match
candidate range [lo, hi) âž” first index whose value is at least target âž” look: return that index, or -1-1 when it is absent.| 1 | from typing import Callable, List |
| 2 | Â |
| 3 | Â |
| 4 | def binary_search_lower_bound(lo: int, hi: int, is_feasible: Callable[[int], bool]) -> int: |
| 5 | """First candidate in [lo, hi) where is_feasible is True. |
| 6 | Precondition: is_feasible is monotonic (False, False, ..., True, True). |
| 7 | Returns hi when no candidate is True. N = hi - lo candidates. T = the cost of one is_feasible call. |
| 8 | Time: O(T * log N) | Space: O(1) |
| 9 | """ |
| 10 | while lo < hi: |
| 11 | mid = lo + (hi - lo) // 2 |
| 12 | Â |
| 13 | # 1. Check the Midpoint: |
| 14 | # is_feasible(mid) shows which side of the flip mid is on: before the answer (False), |
| 15 | # or at the answer or after it (True). |
| 16 | if is_feasible(mid): |
| 17 | # 2. Keep the Candidate: |
| 18 | # mid may be the first True, so keep it in the range by moving hi down to mid. |
| 19 | hi = mid |
| 20 | else: |
| 21 | # 3. Discard the Candidate: |
| 22 | # mid is False, so the first True must be strictly to the right of mid. |
| 23 | lo = mid + 1 |
| 24 | Â |
| 25 | # 4. Return the Boundary: |
| 26 | # The loop stops when lo equals hi; that position is the first True, or hi when none was True. |
| 27 | return lo |
lo holds a value smaller than target, and hi is len(nums) or holds a value at least target, so the first index whose value is at least target is between lo and hi.If target is in the sorted list nums, it is at the first index whose value is at least target. So call binary_search_lower_bound(lo, hi, at_least_target) with lo = 0 and hi = len(nums), and look at nums[i] once. Every index before i holds a value smaller than target, and nums[i] is at least target. So nums[i] == target answers the whole question: equal means i is the first copy, and larger means target is not in the list. The trap is i == len(nums), which Lower Bound returns when every value is smaller than target. Then nums[i] has no value and raises an IndexError, so check i < len(nums) before nums[i] == target.
See it run
Exact Match, step by step
lo 0hi 6mid -
Each column is one index. T means True and F means False. The last column is hi at the start: one position after the last index, and it is never tested.
lothe left end of the range. Every candidate before lo is False.hithe right end of the range. hi may be the answer, and it is never tested.midthe candidate that this pass testsmid = lomid and lo are the same candidateithe index that Lower Bound returned, which gets one lookanswerthe index where target is found. When target is not found, no index is marked and the answer is -1.
Start: lo = 0 and hi = 6. The answer is somewhere from lo to hi, and hi is never tested.
Gotcha 1 of 4
- The trap
Gotcha 1: Reading
nums[i]without checkingi < len(nums)Mistakeif nums[i] == target:Fixif i < len(nums) and nums[i] == target:What happens: Lower Bound returns
len(nums)when every value is smaller thantarget. There is no value at that index, sonums[i]raises anIndexError. Checki < len(nums)first.Example: nums = [1, 3, 5, 6], target = 7Mistake: raises IndexErrorFix: returns -1
- The target may be missing and you must still return a position (where it would be inserted), as in Search Insert Position (LC 35), not
-1. See Lower Bound - You need the last copy of a repeated value, not the first, as in Find First and Last Position of Element in Sorted Array (LC 34). See Upper Bound
- The values fill a grid whose rows continue one another, as in Search a 2D Matrix (LC 74): search the flat index and turn it into a row and a column. See Global Matrix Bisection
Theory
The question we build
What we know
nums is sorted in ascending order.
So the answers to the question "is nums[i] at least target?" are False up to some index, and True from that index on.
What the problem gives us
The input is nums and target.
The check needs only nums[i] and target, so it answers for one index at a time.
The question we build
We ask each index one yes-or-no question: "is nums[i] at least target?"
We use nums = [-1, 0, 3, 5, 9, 12], the input of Binary Search (LC 704).
For target = 9 (Example 1), the answers are False, False, False, False, True, True. The first True is at index 4, and nums[4] is 9, so the answer is 4.
For target = 2 (Example 2), the answers are False, False, True, True, True, True. The first True is at index 2, but nums[2] is 3, not 2. So the answer is -1.
target = 9: the first True is index 4, and it holds 9.
target = 2: the first True is index 2, but it holds 3, so 2 is not in the list.
Narrowing down to the template
The candidates are the indices 0 to len(nums) - 1, so lo = 0 and hi = len(nums).
The call is binary_search_lower_bound(lo, hi, at_least_target).
It returns the first True, and one look at that index decides the answer.
Step 1: Linear scan
The answer is the index of the first value that is equal to target.
The scan states this in one line, and it reads up to every value.
| 1 | def exact_scan(nums, target): |
| 2 | return nums.index(target) if target in nums else -1 |
Cost: O(N) time, where N = len(nums).
Step 2: Lower Bound, then one look
Lower Bound finds the one index where target could be, without reading every value.
One comparison at that index confirms it.
| 1 | def exact_match(nums, target): |
| 2 | def at_least_target(i): |
| 3 | return nums[i] >= target |
| 4 | Â |
| 5 | lo = 0 |
| 6 | hi = len(nums) |
| 7 | i = binary_search_lower_bound(lo, hi, at_least_target) |
| 8 | if i < len(nums) and nums[i] == target: |
| 9 | return i |
| 10 | return -1 |
Cost: O(log N) time and O(1) space.
The shorter version
| 1 | import bisect |
| 2 | Â |
| 3 | def exact_match_short(nums, target): |
| 4 | n = len(nums) |
| 5 | i = bisect.bisect_left(nums, target) |
| 6 | return i if i < n and nums[i] == target else -1 |
bisect_left(nums, target) returns the same index as binary_search_lower_bound(0, len(nums), at_least_target).
The look after it is the same: i < n and nums[i] == target.
Duplicates and the last copy
When target appears several times, search returns the first copy. For nums = [1, 2, 2, 2, 3] and target = 2, it returns 1.
A loop that returns as soon as nums[mid] == target can return index 2 for the same input.
To return the last copy, use the Upper Bound card. It finds the last index whose value is at most target, and one look at that index decides the answer.
Find First and Last Position of Element in Sorted Array (LC 34) asks for both copies. Lower Bound gives the first copy, and Upper Bound gives the last copy.
Real-world use
A read-only lookup table kept as a sorted array, such as a sorted list of IDs loaded from disk, answers "is this ID present, and at which position?" with this search. It needs no extra memory for a hash index. Lower Bound returns the position where the ID would be. The one look tells whether the ID is there. A missing ID is reported as missing, and it is never confused with the ID next to it.
If another thread inserts into nums or deletes from it while the search runs, nums can be out of order at that moment, and the search can miss target. Search a snapshot of the data, or hold a read lock.
Say it in an interview
"I use Lower Bound with the check
nums[i] >= target, because the answers are False before the position oftargetand True from it on. I run it overlo = 0andhi = len(nums), and it returns the first indexiwhose value is at leasttarget. Then I look at that one index. Ifi < len(nums)andnums[i] == target, I returni, and otherwise I return-1. I checki < len(nums)first, becauseiequalslen(nums)when every value is smaller thantarget. The time isO(log N)and the space isO(1)."
Common Questions
- How do I count how many times
targetappears? Find the first index with Lower Bound, and find the index after the last copy with Lower Bound on
target + 1. The count is the difference of the two indices. Both searches costO(log N).- What if the length of the list is unknown?
Set
hi = 1and double it until the value athiis at leasttarget. A read past the end must return a value larger than every real value. Then run Lower Bound fromhi // 2tohi + 1, and make the same look. Search in a Sorted Array of Unknown Size (LC 702) uses this.
Practice
Form 1 of 2
Most books teach Exact Match as its own loop. The loop keeps a closed range [lo, hi], compares nums[mid] with target on every pass, and returns as soon as it finds target. This course reuses the Lower Bound loop, and looks at one index at the end.
The classic loop
| 1 | from typing import List |
| 2 | Â |
| 3 | def binary_search_exact(nums: List[int], target: int) -> int: |
| 4 | """Binary Search: find `target` in sorted `nums` on the closed interval [lo, hi]. |
| 5 | Standard exact-match binary search on closed interval [lo, hi]. |
| 6 | Time: O(log N) | Space: O(1) |
| 7 | """ |
| 8 | lo, hi = 0, len(nums) - 1 |
| 9 | Â |
| 10 | while lo <= hi: |
| 11 | mid = lo + (hi - lo) // 2 # Prevents integer overflow in C++/Java |
| 12 | if nums[mid] == target: |
| 13 | return mid |
| 14 | elif nums[mid] < target: |
| 15 | lo = mid + 1 # Discard mid; target strictly to the right |
| 16 | else: |
| 17 | hi = mid - 1 # Discard mid; target strictly to the left |
| 18 | Â |
| 19 | return -1 |
From the classic loop to our template
| Old Way line | In our template | Why |
|---|---|---|
| lo, hi = 0, len(nums) - 1 | lo = 0 and hi = len(nums) | The classic range is closed, so hi is the last index. The template's hi is one past the last index. |
| while lo <= hi: | while lo < hi: (inside the template) | A half-open range is empty when lo is equal to hi. |
| if nums[mid] == target: return mid | if i < len(nums) and nums[i] == target: return i | The classic loop checks for target on every pass. The template checks once, after the search. |
| elif nums[mid] < target: lo = mid + 1 | at_least_target(mid) is False, so lo = mid + 1 | The same fact, written as a check. |
| else: hi = mid - 1 | at_least_target(mid) is True, so hi = mid | The template keeps mid, because mid may be the first index that holds target. |
| return -1 | return -1 (after the look) | -1 is returned when the look finds another value, or no value. |
When the classic form is fine
Use it when an interviewer asks for this form, or when the code base already uses it.
It can stop early when it finds target, so it may do fewer comparisons for one lookup.
Gotchas of the classic loop
Gotcha 1 of 4
- The trap
Gotcha 1: Writing
hi = midin the closed-range loopMistakehi = midFixhi = mid - 1What happens: The range
[lo, hi]is closed, sonums[mid]has already been checked. Whenlo == hiandnums[mid]is larger thantarget,hi = midleaveshiwhere it was. The range never gets smaller, so the loop never ends.hi = midbelongs to the boundary search, which loopswhile lo < hi.Example: Binary Search (LC 704): nums = [-1, 0, 3, 5, 9, 12], target = 2Mistake: the loop never endsFix: returns -1
Earlier look-alikes
The target may be missing and you must still return a position (where it would be inserted), as in Search Insert Position (LC 35), not -1.
The sorted values fill a grid whose rows continue one another, as in Search a 2D Matrix (LC 74): the positions are cells, so each mid must first be turned into a row and a column.
Earlier description
nums is sorted, so one comparison of nums[mid] with target tells you which half can still hold it: return mid on a match, otherwise drop mid and the wrong half with lo = mid + 1 or hi = mid - 1. Every index outside [lo, hi] has been ruled out, so once lo > hi nothing is left to check and -1 is the right answer. The trap is mixing styles: the closed range [lo, hi] needs while lo <= hi together with both mid + 1 and mid - 1; writing hi = mid, as a boundary search does, loops forever once lo == hi and nums[mid] is too big.
Earlier template
| 1 | from typing import List |
| 2 | Â |
| 3 | def binary_search_exact(nums: List[int], target: int) -> int: |
| 4 | """Binary Search: find `target` in sorted `nums` on the closed interval [lo, hi]. |
| 5 | Standard exact-match binary search on closed interval [lo, hi]. |
| 6 | Time: O(log N) | Space: O(1) |
| 7 | """ |
| 8 | lo, hi = 0, len(nums) - 1 |
| 9 | Â |
| 10 | while lo <= hi: |
| 11 | mid = lo + (hi - lo) // 2 # Prevents integer overflow in C++/Java |
| 12 | if nums[mid] == target: |
| 13 | return mid |
| 14 | elif nums[mid] < target: |
| 15 | lo = mid + 1 # Discard mid; target strictly to the right |
| 16 | else: |
| 17 | hi = mid - 1 # Discard mid; target strictly to the left |
| 18 | Â |
| 19 | return -1 |
Earlier invariant
If target is in nums, its index is always inside [lo, hi].
Earlier direction
candidate domain âž” check midpoint == target âž” early return
Earlier gotchas
Writing hi = mid in the closed-range loop
The range [lo, hi] is closed, so nums[mid] has already been checked. When lo == hi and nums[mid] is larger than target, hi = mid leaves hi where it was. The range never gets smaller, so the loop never ends. hi = mid belongs to the boundary search, which loops while lo < hi.
Starting hi at len(nums) instead of len(nums) - 1
The closed range [lo, hi] reads nums[hi] when lo reaches it. len(nums) is one past the last index. When target is larger than every value, lo climbs to len(nums). Then nums[mid] is nums[len(nums)], and Python raises an IndexError.
Looping while lo < hi in the closed range
When lo == hi, one candidate is still left, and it has not been checked. while lo < hi stops before checking it. So a target that sits at that last index is reported as missing, and the function returns -1.
Writing (lo + hi) / 2 in Java or C++
In Java and C++, an int holds values up to 231 - 1. When lo and hi are both large, lo + hi is larger than that limit. In Java, the sum wraps around to a negative number, so mid is a negative index. In C++, an int that overflows is undefined behaviour. lo + (hi - lo) / 2 never computes a value larger than hi. Python integers have no size limit, so both forms work in Python.
Form 2 of 2
The older solution to Search a 2D Matrix (LC 74) keeps a closed range [lo, hi] over the flat index 0 to m * n - 1, and returns as soon as it finds target.
This course uses the same template as the 1D search. Lower Bound runs over the flat index range [0, m * n), and one look follows.
The classic loop
| 1 | from typing import List |
| 2 | Â |
| 3 | def search_matrix(matrix: List[List[int]], target: int) -> bool: |
| 4 | """Search a 2D Matrix: Classic Binary Search on the flat row-major index. |
| 5 | Time: O(log(M * N)) | Space: O(1) |
| 6 | """ |
| 7 | m, n = len(matrix), len(matrix[0]) |
| 8 | lo, hi = 0, m * n - 1 # Every cell, as one sorted list |
| 9 | Â |
| 10 | while lo <= hi: |
| 11 | mid = lo + (hi - lo) // 2 |
| 12 | row, col = divmod(mid, n) # n cells per row: divide by n, not m |
| 13 | val = matrix[row][col] |
| 14 | if val == target: |
| 15 | return True |
| 16 | elif val < target: |
| 17 | lo = mid + 1 # target is further along the list |
| 18 | else: |
| 19 | hi = mid - 1 # target is earlier in the list |
| 20 | Â |
| 21 | return False |
From the classic loop to our template
| Old Way line | In our template | Why |
|---|---|---|
| lo, hi = 0, m * n - 1 | lo = 0 and hi = m * n | The classic range is closed, so hi is the last flat index. The template's hi is one past the last flat index. |
| while lo <= hi: | while lo < hi: (inside the template) | A half-open range is empty when lo is equal to hi. |
| row, col = divmod(mid, n) val = matrix[row][col] | at_least_target(i) is matrix[i // n][i % n] >= target | The same cell is read. The check asks if it is at least target, and Lower Bound uses the answer. |
| if val == target: return True | if i < m * n and matrix[i // n][i % n] == target: return True | The classic loop checks for target on every pass. The template checks once, after the search, and checks i < m * n first. |
| lo = mid + 1 or hi = mid - 1 | lo = mid + 1 or hi = mid (inside the template) | The template keeps mid when the check is True, because mid may be the first cell that is at least target. |
| return False | return False (after the look) | False is returned when the look finds another value, or no cell. |
When the classic form is fine
Use it when an interviewer asks for this form, or when the code base already uses it.
It can stop early when it finds target, so it may do fewer comparisons for one lookup.
Gotchas of the classic loop
Gotcha 1 of 2
- The trap
Gotcha 1: Writing
divmod(mid, m)instead ofdivmod(mid, n)Mistakerow, col = divmod(mid, m)Fixrow, col = divmod(mid, n)What happens: Each row holds
ncells, wherenis the number of columns. So flat indexmidis in rowmid // n, at columnmid % n. On a 3 x 4 matrix, flat index 5 ismatrix[1][1].divmod(5, 3)gives(1, 2)and readsmatrix[1][2]instead. Fortarget = 60,midreaches 10, anddivmod(10, 3)asks for row 3, which does not exist. A square matrix hasm == n, so it hides this mistake.Example: Search a 2D Matrix (LC 74): matrix = [[1, 3, 5, 7], [10, 11, 16, 20], [23, 30, 34, 60]], target = 60Mistake: raises IndexErrorFix: returns True
Earlier signals
A matrix is sorted along each row and every row starts after the previous row ends, so read row by row it is one sorted list, and you must find a value in log time.
Earlier invariant
If target is in matrix, its flat index is inside [lo, hi], and flat index mid is the cell matrix[mid // n][mid % n].
Earlier description
Because every row starts after the previous row ends, reading matrix row by row gives one sorted list of m * n values, so run Exact Match on that list's positions without building it: lo, hi = 0, m * n - 1, and turn each mid into a cell with row, col = divmod(mid, n). This is correct because position mid has exactly mid // n full rows before it and sits mid % n cells into its own row, so matrix[row][col] is the value the flat list would hold there, and each comparison of val with target drops mid and the half that cannot hold it. The trap is the divisor: it is n, the number of columns (the length of one row), not m. divmod(mid, m) still works on a square matrix, so the bug hides until a non-square one, where it reads the wrong cell or asks for a row that does not exist.
Earlier template
| 1 | from typing import List |
| 2 | Â |
| 3 | def search_matrix(matrix: List[List[int]], target: int) -> bool: |
| 4 | """Search a 2D Matrix: Classic Binary Search on the flat row-major index. |
| 5 | Time: O(log(M * N)) | Space: O(1) |
| 6 | """ |
| 7 | m, n = len(matrix), len(matrix[0]) |
| 8 | lo, hi = 0, m * n - 1 # Every cell, as one sorted list |
| 9 | Â |
| 10 | while lo <= hi: |
| 11 | mid = lo + (hi - lo) // 2 |
| 12 | row, col = divmod(mid, n) # n cells per row: divide by n, not m |
| 13 | val = matrix[row][col] |
| 14 | if val == target: |
| 15 | return True |
| 16 | elif val < target: |
| 17 | lo = mid + 1 # target is further along the list |
| 18 | else: |
| 19 | hi = mid - 1 # target is earlier in the list |
| 20 | Â |
| 21 | return False |
Earlier gotchas
Writing divmod(mid, m) instead of divmod(mid, n)
Each row holds n cells, where n is the number of columns. So flat index mid is in row mid // n, at column mid % n. On a 3 x 4 matrix, flat index 5 is matrix[1][1]. divmod(5, 3) gives (1, 2) and reads matrix[1][2] instead. For target = 60, mid reaches 10, and divmod(10, 3) asks for row 3, which does not exist. A square matrix has m == n, so it hides this mistake.
Starting hi at m * n instead of m * n - 1
The last flat index is m * n - 1. The closed range [lo, hi] reads the cell at hi when lo reaches it. When target is larger than every value, mid reaches m * n. divmod(m * n, n) asks for row m, which does not exist, so Python raises an IndexError.
Picking the row with a separate binary search first
A search down the first column can pick the row, and a second search can look inside that row. This also runs in O(log(m * n)) time. But picking the row is a last-True search. It looks for the last row whose first value is at most target. That search is the Upper Bound card, and it has its own off-by-one risk. For example, a target below matrix[0][0] has no row at all. One search on the flat index has no row step to get wrong.
Walking from the top-right corner
The corner walk starts at the top-right cell. Each comparison drops one row or one column. It is correct, but it takes up to m + n steps. The problem asks for O(log(m * n)) time, which only the binary search meets.
Earlier look-alikes
Rows and columns are each sorted, but a row can start below the end of the row above, as in Search a 2D Matrix II (LC 240): read row by row it is not one sorted list, so there is no flat index to binary search.
You need the k-th smallest value, not whether one value is present, and only the rows and columns are sorted, as in Kth Smallest Element in a Sorted Matrix (LC 378).
Earlier problems
Search a 2D Matrix
Builds on Lower Bound, which you haven’t finished yet.
Classic Binary Search
target, and then checks whether that value is equal to target.Binary Search (LC 704)(opens leetcode.com)
nums is an integer array sorted in ascending order, and target is an integer; find where target sits in nums. Return that index if target is present, or -1 if it isn't.
Your algorithm must run in O(log n) time.
Exact Match
candidate range [lo, hi) âž” first index whose value is at least target âž” look: return that index, or -1-1 when it is absent.| 1 | from typing import Callable, List |
| 2 | Â |
| 3 | Â |
| 4 | def binary_search_lower_bound(lo: int, hi: int, is_feasible: Callable[[int], bool]) -> int: |
| 5 | """First candidate in [lo, hi) where is_feasible is True. |
| 6 | Precondition: is_feasible is monotonic (False, False, ..., True, True). |
| 7 | Returns hi when no candidate is True. N = hi - lo candidates. T = the cost of one is_feasible call. |
| 8 | Time: O(T * log N) | Space: O(1) |
| 9 | """ |
| 10 | while lo < hi: |
| 11 | mid = lo + (hi - lo) // 2 |
| 12 | Â |
| 13 | # 1. Check the Midpoint: |
| 14 | # is_feasible(mid) shows which side of the flip mid is on: before the answer (False), |
| 15 | # or at the answer or after it (True). |
| 16 | if is_feasible(mid): |
| 17 | # 2. Keep the Candidate: |
| 18 | # mid may be the first True, so keep it in the range by moving hi down to mid. |
| 19 | hi = mid |
| 20 | else: |
| 21 | # 3. Discard the Candidate: |
| 22 | # mid is False, so the first True must be strictly to the right of mid. |
| 23 | lo = mid + 1 |
| 24 | Â |
| 25 | # 4. Return the Boundary: |
| 26 | # The loop stops when lo equals hi; that position is the first True, or hi when none was True. |
| 27 | return lo |
lo holds a value smaller than target, and hi is len(nums) or holds a value at least target, so the first index whose value is at least target is between lo and hi.If target is in the sorted list nums, it is at the first index whose value is at least target. So call binary_search_lower_bound(lo, hi, at_least_target) with lo = 0 and hi = len(nums), and look at nums[i] once. Every index before i holds a value smaller than target, and nums[i] is at least target. So nums[i] == target answers the whole question: equal means i is the first copy, and larger means target is not in the list. The trap is i == len(nums), which Lower Bound returns when every value is smaller than target. Then nums[i] has no value and raises an IndexError, so check i < len(nums) before nums[i] == target.
See it run
Exact Match, step by step
lo 0hi 6mid -
Each column is one index. T means True and F means False. The last column is hi at the start: one position after the last index, and it is never tested.
lothe left end of the range. Every candidate before lo is False.hithe right end of the range. hi may be the answer, and it is never tested.midthe candidate that this pass testsmid = lomid and lo are the same candidateithe index that Lower Bound returned, which gets one lookanswerthe index where target is found. When target is not found, no index is marked and the answer is -1.
Start: lo = 0 and hi = 6. The answer is somewhere from lo to hi, and hi is never tested.
Gotcha 1 of 4
- The trap
Gotcha 1: Reading
nums[i]without checkingi < len(nums)Mistakeif nums[i] == target:Fixif i < len(nums) and nums[i] == target:What happens: Lower Bound returns
len(nums)when every value is smaller thantarget. There is no value at that index, sonums[i]raises anIndexError. Checki < len(nums)first.Example: nums = [1, 3, 5, 6], target = 7Mistake: raises IndexErrorFix: returns -1
- The target may be missing and you must still return a position (where it would be inserted), as in Search Insert Position (LC 35), not
-1. See Lower Bound - You need the last copy of a repeated value, not the first, as in Find First and Last Position of Element in Sorted Array (LC 34). See Upper Bound
- The values fill a grid whose rows continue one another, as in Search a 2D Matrix (LC 74): search the flat index and turn it into a row and a column. See Global Matrix Bisection
Theory
The question we build
What we know
nums is sorted in ascending order.
So the answers to the question "is nums[i] at least target?" are False up to some index, and True from that index on.
What the problem gives us
The input is nums and target.
The check needs only nums[i] and target, so it answers for one index at a time.
The question we build
We ask each index one yes-or-no question: "is nums[i] at least target?"
We use nums = [-1, 0, 3, 5, 9, 12], the input of Binary Search (LC 704).
For target = 9 (Example 1), the answers are False, False, False, False, True, True. The first True is at index 4, and nums[4] is 9, so the answer is 4.
For target = 2 (Example 2), the answers are False, False, True, True, True, True. The first True is at index 2, but nums[2] is 3, not 2. So the answer is -1.
target = 9: the first True is index 4, and it holds 9.
target = 2: the first True is index 2, but it holds 3, so 2 is not in the list.
Narrowing down to the template
The candidates are the indices 0 to len(nums) - 1, so lo = 0 and hi = len(nums).
The call is binary_search_lower_bound(lo, hi, at_least_target).
It returns the first True, and one look at that index decides the answer.
Step 1: Linear scan
The answer is the index of the first value that is equal to target.
The scan states this in one line, and it reads up to every value.
| 1 | def exact_scan(nums, target): |
| 2 | return nums.index(target) if target in nums else -1 |
Cost: O(N) time, where N = len(nums).
Step 2: Lower Bound, then one look
Lower Bound finds the one index where target could be, without reading every value.
One comparison at that index confirms it.
| 1 | def exact_match(nums, target): |
| 2 | def at_least_target(i): |
| 3 | return nums[i] >= target |
| 4 | Â |
| 5 | lo = 0 |
| 6 | hi = len(nums) |
| 7 | i = binary_search_lower_bound(lo, hi, at_least_target) |
| 8 | if i < len(nums) and nums[i] == target: |
| 9 | return i |
| 10 | return -1 |
Cost: O(log N) time and O(1) space.
The shorter version
| 1 | import bisect |
| 2 | Â |
| 3 | def exact_match_short(nums, target): |
| 4 | n = len(nums) |
| 5 | i = bisect.bisect_left(nums, target) |
| 6 | return i if i < n and nums[i] == target else -1 |
bisect_left(nums, target) returns the same index as binary_search_lower_bound(0, len(nums), at_least_target).
The look after it is the same: i < n and nums[i] == target.
Duplicates and the last copy
When target appears several times, search returns the first copy. For nums = [1, 2, 2, 2, 3] and target = 2, it returns 1.
A loop that returns as soon as nums[mid] == target can return index 2 for the same input.
To return the last copy, use the Upper Bound card. It finds the last index whose value is at most target, and one look at that index decides the answer.
Find First and Last Position of Element in Sorted Array (LC 34) asks for both copies. Lower Bound gives the first copy, and Upper Bound gives the last copy.
Real-world use
A read-only lookup table kept as a sorted array, such as a sorted list of IDs loaded from disk, answers "is this ID present, and at which position?" with this search. It needs no extra memory for a hash index. Lower Bound returns the position where the ID would be. The one look tells whether the ID is there. A missing ID is reported as missing, and it is never confused with the ID next to it.
If another thread inserts into nums or deletes from it while the search runs, nums can be out of order at that moment, and the search can miss target. Search a snapshot of the data, or hold a read lock.
Say it in an interview
"I use Lower Bound with the check
nums[i] >= target, because the answers are False before the position oftargetand True from it on. I run it overlo = 0andhi = len(nums), and it returns the first indexiwhose value is at leasttarget. Then I look at that one index. Ifi < len(nums)andnums[i] == target, I returni, and otherwise I return-1. I checki < len(nums)first, becauseiequalslen(nums)when every value is smaller thantarget. The time isO(log N)and the space isO(1)."
Common Questions
- How do I count how many times
targetappears? Find the first index with Lower Bound, and find the index after the last copy with Lower Bound on
target + 1. The count is the difference of the two indices. Both searches costO(log N).- What if the length of the list is unknown?
Set
hi = 1and double it until the value athiis at leasttarget. A read past the end must return a value larger than every real value. Then run Lower Bound fromhi // 2tohi + 1, and make the same look. Search in a Sorted Array of Unknown Size (LC 702) uses this.
Practice
Form 1 of 2
Most books teach Exact Match as its own loop. The loop keeps a closed range [lo, hi], compares nums[mid] with target on every pass, and returns as soon as it finds target. This course reuses the Lower Bound loop, and looks at one index at the end.
The classic loop
| 1 | from typing import List |
| 2 | Â |
| 3 | def binary_search_exact(nums: List[int], target: int) -> int: |
| 4 | """Binary Search: find `target` in sorted `nums` on the closed interval [lo, hi]. |
| 5 | Standard exact-match binary search on closed interval [lo, hi]. |
| 6 | Time: O(log N) | Space: O(1) |
| 7 | """ |
| 8 | lo, hi = 0, len(nums) - 1 |
| 9 | Â |
| 10 | while lo <= hi: |
| 11 | mid = lo + (hi - lo) // 2 # Prevents integer overflow in C++/Java |
| 12 | if nums[mid] == target: |
| 13 | return mid |
| 14 | elif nums[mid] < target: |
| 15 | lo = mid + 1 # Discard mid; target strictly to the right |
| 16 | else: |
| 17 | hi = mid - 1 # Discard mid; target strictly to the left |
| 18 | Â |
| 19 | return -1 |
From the classic loop to our template
| Old Way line | In our template | Why |
|---|---|---|
| lo, hi = 0, len(nums) - 1 | lo = 0 and hi = len(nums) | The classic range is closed, so hi is the last index. The template's hi is one past the last index. |
| while lo <= hi: | while lo < hi: (inside the template) | A half-open range is empty when lo is equal to hi. |
| if nums[mid] == target: return mid | if i < len(nums) and nums[i] == target: return i | The classic loop checks for target on every pass. The template checks once, after the search. |
| elif nums[mid] < target: lo = mid + 1 | at_least_target(mid) is False, so lo = mid + 1 | The same fact, written as a check. |
| else: hi = mid - 1 | at_least_target(mid) is True, so hi = mid | The template keeps mid, because mid may be the first index that holds target. |
| return -1 | return -1 (after the look) | -1 is returned when the look finds another value, or no value. |
When the classic form is fine
Use it when an interviewer asks for this form, or when the code base already uses it.
It can stop early when it finds target, so it may do fewer comparisons for one lookup.
Gotchas of the classic loop
Gotcha 1 of 4
- The trap
Gotcha 1: Writing
hi = midin the closed-range loopMistakehi = midFixhi = mid - 1What happens: The range
[lo, hi]is closed, sonums[mid]has already been checked. Whenlo == hiandnums[mid]is larger thantarget,hi = midleaveshiwhere it was. The range never gets smaller, so the loop never ends.hi = midbelongs to the boundary search, which loopswhile lo < hi.Example: Binary Search (LC 704): nums = [-1, 0, 3, 5, 9, 12], target = 2Mistake: the loop never endsFix: returns -1
Earlier look-alikes
The target may be missing and you must still return a position (where it would be inserted), as in Search Insert Position (LC 35), not -1.
The sorted values fill a grid whose rows continue one another, as in Search a 2D Matrix (LC 74): the positions are cells, so each mid must first be turned into a row and a column.
Earlier description
nums is sorted, so one comparison of nums[mid] with target tells you which half can still hold it: return mid on a match, otherwise drop mid and the wrong half with lo = mid + 1 or hi = mid - 1. Every index outside [lo, hi] has been ruled out, so once lo > hi nothing is left to check and -1 is the right answer. The trap is mixing styles: the closed range [lo, hi] needs while lo <= hi together with both mid + 1 and mid - 1; writing hi = mid, as a boundary search does, loops forever once lo == hi and nums[mid] is too big.
Earlier template
| 1 | from typing import List |
| 2 | Â |
| 3 | def binary_search_exact(nums: List[int], target: int) -> int: |
| 4 | """Binary Search: find `target` in sorted `nums` on the closed interval [lo, hi]. |
| 5 | Standard exact-match binary search on closed interval [lo, hi]. |
| 6 | Time: O(log N) | Space: O(1) |
| 7 | """ |
| 8 | lo, hi = 0, len(nums) - 1 |
| 9 | Â |
| 10 | while lo <= hi: |
| 11 | mid = lo + (hi - lo) // 2 # Prevents integer overflow in C++/Java |
| 12 | if nums[mid] == target: |
| 13 | return mid |
| 14 | elif nums[mid] < target: |
| 15 | lo = mid + 1 # Discard mid; target strictly to the right |
| 16 | else: |
| 17 | hi = mid - 1 # Discard mid; target strictly to the left |
| 18 | Â |
| 19 | return -1 |
Earlier invariant
If target is in nums, its index is always inside [lo, hi].
Earlier direction
candidate domain âž” check midpoint == target âž” early return
Earlier gotchas
Writing hi = mid in the closed-range loop
The range [lo, hi] is closed, so nums[mid] has already been checked. When lo == hi and nums[mid] is larger than target, hi = mid leaves hi where it was. The range never gets smaller, so the loop never ends. hi = mid belongs to the boundary search, which loops while lo < hi.
Starting hi at len(nums) instead of len(nums) - 1
The closed range [lo, hi] reads nums[hi] when lo reaches it. len(nums) is one past the last index. When target is larger than every value, lo climbs to len(nums). Then nums[mid] is nums[len(nums)], and Python raises an IndexError.
Looping while lo < hi in the closed range
When lo == hi, one candidate is still left, and it has not been checked. while lo < hi stops before checking it. So a target that sits at that last index is reported as missing, and the function returns -1.
Writing (lo + hi) / 2 in Java or C++
In Java and C++, an int holds values up to 231 - 1. When lo and hi are both large, lo + hi is larger than that limit. In Java, the sum wraps around to a negative number, so mid is a negative index. In C++, an int that overflows is undefined behaviour. lo + (hi - lo) / 2 never computes a value larger than hi. Python integers have no size limit, so both forms work in Python.
Form 2 of 2
The older solution to Search a 2D Matrix (LC 74) keeps a closed range [lo, hi] over the flat index 0 to m * n - 1, and returns as soon as it finds target.
This course uses the same template as the 1D search. Lower Bound runs over the flat index range [0, m * n), and one look follows.
The classic loop
| 1 | from typing import List |
| 2 | Â |
| 3 | def search_matrix(matrix: List[List[int]], target: int) -> bool: |
| 4 | """Search a 2D Matrix: Classic Binary Search on the flat row-major index. |
| 5 | Time: O(log(M * N)) | Space: O(1) |
| 6 | """ |
| 7 | m, n = len(matrix), len(matrix[0]) |
| 8 | lo, hi = 0, m * n - 1 # Every cell, as one sorted list |
| 9 | Â |
| 10 | while lo <= hi: |
| 11 | mid = lo + (hi - lo) // 2 |
| 12 | row, col = divmod(mid, n) # n cells per row: divide by n, not m |
| 13 | val = matrix[row][col] |
| 14 | if val == target: |
| 15 | return True |
| 16 | elif val < target: |
| 17 | lo = mid + 1 # target is further along the list |
| 18 | else: |
| 19 | hi = mid - 1 # target is earlier in the list |
| 20 | Â |
| 21 | return False |
From the classic loop to our template
| Old Way line | In our template | Why |
|---|---|---|
| lo, hi = 0, m * n - 1 | lo = 0 and hi = m * n | The classic range is closed, so hi is the last flat index. The template's hi is one past the last flat index. |
| while lo <= hi: | while lo < hi: (inside the template) | A half-open range is empty when lo is equal to hi. |
| row, col = divmod(mid, n) val = matrix[row][col] | at_least_target(i) is matrix[i // n][i % n] >= target | The same cell is read. The check asks if it is at least target, and Lower Bound uses the answer. |
| if val == target: return True | if i < m * n and matrix[i // n][i % n] == target: return True | The classic loop checks for target on every pass. The template checks once, after the search, and checks i < m * n first. |
| lo = mid + 1 or hi = mid - 1 | lo = mid + 1 or hi = mid (inside the template) | The template keeps mid when the check is True, because mid may be the first cell that is at least target. |
| return False | return False (after the look) | False is returned when the look finds another value, or no cell. |
When the classic form is fine
Use it when an interviewer asks for this form, or when the code base already uses it.
It can stop early when it finds target, so it may do fewer comparisons for one lookup.
Gotchas of the classic loop
Gotcha 1 of 2
- The trap
Gotcha 1: Writing
divmod(mid, m)instead ofdivmod(mid, n)Mistakerow, col = divmod(mid, m)Fixrow, col = divmod(mid, n)What happens: Each row holds
ncells, wherenis the number of columns. So flat indexmidis in rowmid // n, at columnmid % n. On a 3 x 4 matrix, flat index 5 ismatrix[1][1].divmod(5, 3)gives(1, 2)and readsmatrix[1][2]instead. Fortarget = 60,midreaches 10, anddivmod(10, 3)asks for row 3, which does not exist. A square matrix hasm == n, so it hides this mistake.Example: Search a 2D Matrix (LC 74): matrix = [[1, 3, 5, 7], [10, 11, 16, 20], [23, 30, 34, 60]], target = 60Mistake: raises IndexErrorFix: returns True
Earlier signals
A matrix is sorted along each row and every row starts after the previous row ends, so read row by row it is one sorted list, and you must find a value in log time.
Earlier invariant
If target is in matrix, its flat index is inside [lo, hi], and flat index mid is the cell matrix[mid // n][mid % n].
Earlier description
Because every row starts after the previous row ends, reading matrix row by row gives one sorted list of m * n values, so run Exact Match on that list's positions without building it: lo, hi = 0, m * n - 1, and turn each mid into a cell with row, col = divmod(mid, n). This is correct because position mid has exactly mid // n full rows before it and sits mid % n cells into its own row, so matrix[row][col] is the value the flat list would hold there, and each comparison of val with target drops mid and the half that cannot hold it. The trap is the divisor: it is n, the number of columns (the length of one row), not m. divmod(mid, m) still works on a square matrix, so the bug hides until a non-square one, where it reads the wrong cell or asks for a row that does not exist.
Earlier template
| 1 | from typing import List |
| 2 | Â |
| 3 | def search_matrix(matrix: List[List[int]], target: int) -> bool: |
| 4 | """Search a 2D Matrix: Classic Binary Search on the flat row-major index. |
| 5 | Time: O(log(M * N)) | Space: O(1) |
| 6 | """ |
| 7 | m, n = len(matrix), len(matrix[0]) |
| 8 | lo, hi = 0, m * n - 1 # Every cell, as one sorted list |
| 9 | Â |
| 10 | while lo <= hi: |
| 11 | mid = lo + (hi - lo) // 2 |
| 12 | row, col = divmod(mid, n) # n cells per row: divide by n, not m |
| 13 | val = matrix[row][col] |
| 14 | if val == target: |
| 15 | return True |
| 16 | elif val < target: |
| 17 | lo = mid + 1 # target is further along the list |
| 18 | else: |
| 19 | hi = mid - 1 # target is earlier in the list |
| 20 | Â |
| 21 | return False |
Earlier gotchas
Writing divmod(mid, m) instead of divmod(mid, n)
Each row holds n cells, where n is the number of columns. So flat index mid is in row mid // n, at column mid % n. On a 3 x 4 matrix, flat index 5 is matrix[1][1]. divmod(5, 3) gives (1, 2) and reads matrix[1][2] instead. For target = 60, mid reaches 10, and divmod(10, 3) asks for row 3, which does not exist. A square matrix has m == n, so it hides this mistake.
Starting hi at m * n instead of m * n - 1
The last flat index is m * n - 1. The closed range [lo, hi] reads the cell at hi when lo reaches it. When target is larger than every value, mid reaches m * n. divmod(m * n, n) asks for row m, which does not exist, so Python raises an IndexError.
Picking the row with a separate binary search first
A search down the first column can pick the row, and a second search can look inside that row. This also runs in O(log(m * n)) time. But picking the row is a last-True search. It looks for the last row whose first value is at most target. That search is the Upper Bound card, and it has its own off-by-one risk. For example, a target below matrix[0][0] has no row at all. One search on the flat index has no row step to get wrong.
Walking from the top-right corner
The corner walk starts at the top-right cell. Each comparison drops one row or one column. It is correct, but it takes up to m + n steps. The problem asks for O(log(m * n)) time, which only the binary search meets.
Earlier look-alikes
Rows and columns are each sorted, but a row can start below the end of the row above, as in Search a 2D Matrix II (LC 240): read row by row it is not one sorted list, so there is no flat index to binary search.
You need the k-th smallest value, not whether one value is present, and only the rows and columns are sorted, as in Kth Smallest Element in a Sorted Matrix (LC 378).
Earlier problems
Search a 2D Matrix