Lower Bound (First True Binary Search)
You have an array of distinct integers in sorted order and a target value. If target is already in the array, return its index; otherwise, return the index it would land at if you inserted it while keeping the array sorted.
Solve it in O(log n) time.
Lower Bound
candidate domain âž” monotonic boolean sequence [F, F, ..., T, T] âž” first True| 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 is False, and every candidate from hi on is True or does not exist, so the first True is always in [lo, hi].is_feasible is False before the answer and True from the answer on, so one call at mid decides the move, which is hi = mid after a True and lo = mid + 1 after a False. A True mid may be the first True, so hi = mid keeps it in the range. A False mid is not the answer and the first True is to its right, so lo = mid + 1 drops it. After either move the first True is still in [lo, hi]. hi is allowed to be the answer, because it starts one past the last candidate, so a row with no True returns hi. The range gets smaller on every pass and lo never passes hi, so the loop stops with lo == hi, and lo is the answer. The trap is lo = mid in the False branch. The midpoint is rounded down, so when hi == lo + 1, mid equals lo. Then lo does not move, and the loop never ends.
See it run
Lower Bound, 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 candidateanswerthe first True, or hi when no index is True
Start: lo = 0 and hi = 6. The answer is somewhere from lo to hi, and hi is never tested.
Gotcha 1 of 7
- The trap
Gotcha 1: Writing
lo = midinstead oflo = mid + 1Mistakelo = midFixlo = mid + 1What happens: When one candidate is left (
hiis equal tolo + 1),midequalslo. If the check is False there,lo = midleaveslowhere it was. The range never gets smaller, so the loop never ends.Example: Search Insert Position (LC 35): nums = [1, 3, 5, 6], target = 5Mistake: the loop never endsFix: returns 2
- You only need to know whether
targetis present and any one index of it, as in Binary Search (LC 704), not where a yes/no check first becomes true. See Exact Match - There is no array of candidates to index: the answer is a value you choose (an eating speed) and checking it means simulating the whole input, as in Koko Eating Bananas (LC 875). See Feasible Function
- The question asks for the largest value that still passes a check, as in Sqrt(x) (LC 69), the largest
kwithk * k <= x: the answer is the last True, not the first. See Upper Bound - The values are not sorted, as in H-Index (LC 274). H-Index II (LC 275) has sorted values and fits this card. With unsorted values there is no order to search, and sorting first costs more than counting. See Counting Sort
Theory
The question we build
What we know
nums is sorted, so nums[i] never gets smaller when i gets larger.
One comparison at the middle index therefore shows which side can still hold the answer.
What the problem gives us
The input is nums and target.
The loop needs two things built from them: a check for one index, and the range of candidate indices.
The question we build
We ask each index one yes-or-no question: "is nums[i] at least target?"
The check at_least_target(i) returns nums[i] >= target.
We use nums = [1, 3, 5, 6] and target = 5 (the first example of Search Insert Position (LC 35)).
The answers are False, False, True, True.
The values only get larger. So after one True, every later answer is True too.
The first True is index 2, so target = 5 goes at index 2.
Narrowing down to the template
Every index before the insert position holds a value smaller than target.
The insert position holds a value that is at least target.
So the insert position is the first True of at_least_target.
The candidates are the indices 0 to len(nums) - 1.
So the call is binary_search_lower_bound(0, len(nums), at_least_target).
Step 1: Linear scan
The answer is the first index where the check is True. The scan finds it by checking the indices in order. It is correct, and it states in one line what the answer means.
| 1 | def first_true_scan(lo, hi, is_feasible): |
| 2 | i = lo |
| 3 | while i < hi and not is_feasible(i): |
| 4 | i += 1 |
| 5 | return i # hi when no candidate is True |
Cost: O(N) checks, where N is the number of candidates.
Step 2: Halving loop
The scan checks every candidate.
The halving loop checks only mid, the middle of the range.
The row of answers is False…False True…True, so one check at mid shows which side of mid cannot hold the first True.
The loop discards that side, and the range is about half as long after every pass.
| 1 | def first_true_halving(lo, hi, is_feasible): |
| 2 | while lo < hi: |
| 3 | mid = lo + (hi - lo) // 2 |
| 4 | if is_feasible(mid): |
| 5 | hi = mid # mid may be the first True: keep it |
| 6 | else: |
| 7 | lo = mid + 1 # mid is False: the first True is to the right |
| 8 | return lo |
Cost: O(log N) checks.
Step 3: The named template
The loop does not change between problems. Only the check and the range change.
| 1 | New line: from typing import Callable |
| 2 | |
| 3 | New line: def binary_search_lower_bound(lo: int, hi: int, is_feasible: Callable[[int], bool]) -> int: |
| 4 | New line: """First candidate in [lo, hi) where is_feasible is True. |
| 5 | New line: Precondition: is_feasible is monotonic (False, False, ..., True, True). |
| 6 | New line: Returns hi when no candidate is True. N = hi - lo candidates. T = the cost of one is_feasible call. |
| 7 | New line: Time: O(T * log N) | Space: O(1) |
| 8 | New line: """ |
| 9 | while lo < hi: |
| 10 | mid = lo + (hi - lo) // 2 |
| 11 | |
| 12 | New line: # 1. Check the Midpoint: |
| 13 | New line: # is_feasible(mid) shows which side of the flip mid is on: before the answer (False), |
| 14 | New line: # or at the answer or after it (True). |
| 15 | if is_feasible(mid): |
| 16 | New line: # 2. Keep the Candidate: |
| 17 | New line: # mid may be the first True, so keep it in the range by moving hi down to mid. |
| 18 | hi = mid |
| 19 | else: |
| 20 | New line: # 3. Discard the Candidate: |
| 21 | New line: # mid is False, so the first True must be strictly to the right of mid. |
| 22 | lo = mid + 1 |
| 23 | |
| 24 | New line: # 4. Return the Boundary: |
| 25 | New line: # The loop stops when lo equals hi; that position is the first True, or hi when none was True. |
| 26 | return lo |
Cost: O(T * log N), where T is the cost of one is_feasible call.
| Before | After | What changed |
|---|---|---|
def first_true_halving(lo, hi, is_feasible): | def binary_search_lower_bound(lo: int, hi: int, is_feasible: Callable[[int], bool]) -> int: | A name and types. The loop is the same. |
| the loop body | the loop body | Identical. Only comments change. |
| (none) | the docstring | The contract is written down. |
The shorter version
1. A range of candidates, range(lo, hi)
| 1 | from bisect import bisect_left |
| 2 | Â |
| 3 | # is_feasible must read False, then True. |
| 4 | first = bisect_left(range(lo, hi), True, key=is_feasible) + lo |
bisect_left is Python's own version of this search. The key is_feasible turns each candidate into False or True, and bisect finds where True first appears. Bisect returns a position counted from 0 in the range, so add lo to get the candidate. For lo = 2, hi = 5 and a first True at candidate 3, bisect returns 1, and 1 + 2 = 3. When no candidate is True, the result is hi. An empty range also returns hi. Negative candidate values are allowed, because the position that bisect returns is never negative. Do not pass a negative lo to bisect itself: bisect_left(range(n), True, -1, hi, key=...) raises ValueError and does not count from the end. range does not build a list. The key= argument needs Python 3.10 or later.
2. A sorted list, nums
| 1 | from bisect import bisect_left |
| 2 | Â |
| 3 | # nums is sorted; search indices in [lo, hi). |
| 4 | insert_at = bisect_left(nums, target, lo, hi) |
| 5 | Â |
| 6 | # Records are sorted by their numeric first field. |
| 7 | records = [(1, 'Ada'), (2, 'Bo'), (2, 'Cy'), (4, 'Di')] |
| 8 | first_record = bisect_left(records, 2, 1, 4, key=lambda record: record[0]) # 1 |
bisect_left(nums, target, lo, hi) finds the first index in [lo, hi) where nums[i] >= target. The arguments are the list, the target, then the bounds. The result is already an index in nums, so do not add lo. If the target occurs in the window, the result is its first occurrence. Otherwise it is the insert position, and when no value qualifies it is hi. Indices start at 0, so for the whole list the result also equals the number of values below target. For nums = [1, 2, 2, 2, 4] and target = 2, the whole list gives 1, and the window lo = 2, hi = 5 gives 2. Check insert_at < hi before you read nums[insert_at].
For records, key=lambda record: record[0] reads the numeric field, and the records must already be sorted by that field. Bisect applies the key to the records and not to the target, so the target 2 is already a number. The bounds 1, 4 search the records at indices 1, 2 and 3, and the first record with a key of at least 2 is at index 1. Before you index, check first_record < 4.
The range [lo, hi) and what hi means
The range [lo, hi) is called a half-open range. It includes lo and does not include hi.
A closed range, written [lo, hi], includes both ends.
In [lo, hi), hi is one past the last candidate, and the search never checks hi.
Here are the candidates for nums = [1, 3, 5, 6]:
| Index | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| Value | 1 | 3 | 5 | 6 | none |
| Role | candidate | candidate | candidate | candidate | hi, one past the last candidate |
Because hi is never checked, it can stand for "no candidate is True".
For Search Insert Position (LC 35), that answer means "insert at the end".
Many books write the same search with a closed range: hi = len(nums) - 1 and while lo <= hi.
That loop is in the "Old ways" tab.
The values you pass for lo and hi depend on what you look for.
The template keeps one shape:
| What you look for | lo (first candidate) | hi (one past the last candidate) |
|---|---|---|
The first index with nums[i] >= target | 0 | len(nums) |
The first bad version, with versions 1 to n | 1 | n + 1 |
How to build is_feasible
The check is the only part that changes between problems. Ask four questions, in this order:
- What does True mean? Name it in words, for example
at_least_target. - Is the row
False…False True…True? Draw the row on a tiny input. If the row has another shape, the check is wrong. - Can I evaluate the check for one proposed candidate using the input? State the work one evaluation performs. It may read neighbours, count values, or simulate a process.
- What range
[lo, hi)holds every possible answer?hiis one past the last candidate. It is the answer when no candidate is True.
Here are three worked checks, each drawn on a tiny input.
The first two use nums = [1, 3, 5, 6] and target = 3.
nums[i] >= target is True from the first value that is at least target.
nums[i] > target is True from the first value that is larger than target.
First Bad Version (LC 278) with n = 5. Version 4 is the first bad version. The candidates are the versions 1 to n, so lo = 1 and hi = n + 1.
The contract
| Item | Lower Bound |
|---|---|
| Range | Half-open [lo, hi). hi is one past the last candidate. |
Is hi checked? | Never. mid < hi on every pass. |
| The check | Takes one candidate. Returns True or False. Must be False…False True…True over [lo, hi). |
| Returns | The first True. hi when no candidate is True. |
| Equal values | The first of them, because a True mid is kept and the search continues to the left. |
| Cost | O(T * log N) time and O(1) space. N = hi - lo, and T is the cost of one check. |
A target smaller than every value needs no special case.
Every check reads True, so each pass sets hi = mid. hi reaches lo, and the loop returns 0.
The midpoint is lo + (hi - lo) // 2. It gives the same number as (lo + hi) // 2.
In Java, C++ and C#, lo + hi can be larger than the largest 32-bit integer, and the result then becomes negative.
lo + (hi - lo) // 2 never goes above hi, so it is safe in every language. Python integers have no limit.
Real-world use
This search has standard-library names in several languages. Python's bisect.bisect_left runs this same half-open loop, and C++'s std::lower_bound returns the same position. Both find where a value belongs in a sorted sequence, for example to keep a list sorted while inserting.
Say it in an interview
About 60 seconds, for Search Insert Position (LC 35):
"I use Lower Bound, because the check
nums[i] >= targetis False, then True, so the insert position is the first True. I search[0, len(nums)), solen(nums)can mean insert at the end. A passingmidmay be the answer, sohi = mid, and a failingmidis not, solo = mid + 1. It takesO(log N)time andO(1)space. The trap islo = mid. Whenhi == lo + 1,midequalsloand the loop never ends."
Common Questions
- How do you count how many times
targetappears? Call Lower Bound twice: once with
nums[i] >= targetfor the first index, and once withnums[i] > targetfor the first index after the equal values. The difference between the two results is the count. Fornums = [1, 2, 2, 2, 4]andtarget = 2, the results are 1 and 4, so the count is 3.- How do you find the last index with
nums[i] <= targetusing Lower Bound? Find the first index where
nums[i] > target, then subtract 1. Fornums = [1, 2, 2, 2, 4]andtarget = 2, the first index withnums[i] > 2is 4, so the answer is 3. When every value is greater thantarget, the first index is 0 and the answer is -1. Upper Bound returns the same value with one call.- Why does the loop not return when
nums[mid] == target? Equal values can repeat, and the answer is the first of them. A
midthat equalstargetmay have an equal value on its left, so the loop keepsmidwithhi = midand continues to the left. Fornums = [2, 2, 2]andtarget = 2, the result is 0. An early return at the middle would give 1.
Practice
Most courses teach a new binary-search loop for every problem. This course teaches one loop, and each problem only changes the check that the loop calls.
The classic loop
| 1 | from typing import List |
| 2 | Â |
| 3 | def searchInsert(nums: List[int], target: int) -> int: |
| 4 | """Search Insert Position, the classic closed loop. |
| 5 | Time: O(log N) | Space: O(1) |
| 6 | """ |
| 7 | lo, hi = 0, len(nums) - 1 |
| 8 | while lo <= hi: |
| 9 | mid = lo + (hi - lo) // 2 |
| 10 | if nums[mid] >= target: |
| 11 | hi = mid - 1 |
| 12 | else: |
| 13 | lo = mid + 1 |
| 14 | return lo |
| 1 | def search_insert_recorded(nums, target, ans): |
| 2 | lo, hi = 0, len(nums) - 1 |
| 3 | while lo <= hi: |
| 4 | mid = lo + (hi - lo) // 2 |
| 5 | if nums[mid] >= target: |
| 6 | ans = mid |
| 7 | hi = mid - 1 |
| 8 | else: |
| 9 | lo = mid + 1 |
| 10 | return ans |
From the classic loop to our template
| Old Way line | In our template | Why |
|---|---|---|
| lo, hi = 0, len(nums) - 1 | binary_search_lower_bound(0, len(nums), at_least_target) | The template's hi is one past the last index, so "no value passes" is the answer hi. |
| while lo <= hi: | while lo < hi: | The half-open range is empty when lo is equal to hi. |
| if nums[mid] >= target: | at_least_target(i): return nums[i] >= target | The question moves into a named check. |
| hi = mid - 1 | hi = mid | The closed loop drops mid. In the template, mid can be the answer, so hi = mid keeps it, and no recorded answer is needed. |
| lo = mid + 1 | lo = mid + 1 | The same line in both forms. |
| return lo | insert_at = ..., then return insert_at | The result of the template call gets its own name. |
When the classic form is fine
Use it when an interviewer asks for this form, or when the code base already uses it. The number of steps is about the same.
A second common form keeps the answer in a variable. Write ans = len(nums) before the loop, set ans = mid just before hi = mid - 1, and return ans. It is the same search.
Gotchas of the classic loop
Gotcha 1 of 3
- The trap
Gotcha 1: Writing
hi = midin the closed loopMistakehi = midFixhi = mid - 1What happens: The range
[lo, hi]is closed, somidis checked and then dropped. Withhi = mid, the range keepsmid. Whenlo == hiand the check is True,hidoes not move.lo <= histays True, so the loop never ends.Example: nums = [1, 3, 5, 6], target = 5Mistake: the loop never endsFix: returns 2
Earlier description
is_feasible is False for every candidate before the answer and True from the answer on, so mid only tells you which side of the flip you are on. If is_feasible(mid), mid may be the first True, so keep it with hi = mid; otherwise the first True is strictly to the right, so lo = mid + 1. The first True never leaves [lo, hi] (it is hi itself when nothing is True, since hi starts one past the last candidate), and the range shrinks on every step, so the loop stops at lo == hi on the answer. The trap is writing lo = mid in the False branch: with the floor midpoint, mid == lo when two candidates are left, so lo never moves and the loop never ends.
Earlier signals
The question is 'where does something first become true': insert position, first bad version, first value at least target, how many values are below a threshold.
Earlier invariant
Every candidate below lo is False and every candidate from hi on is True (or past the end), so the first True is always in [lo, hi].
Earlier gotchas
Reading nums[i] without checking i < len(nums)
The search returns a position, not an index that is known to be valid. When no value passes the check, it returns len(nums). Reading nums[len(nums)] raises an IndexError. Find First and Last Position of Element in Sorted Array (LC 34) needs this check before it returns the first position.
Earlier look-alikes
- You only need to know whether
targetis present and any one index of it, as in Binary Search (LC 704), not where a yes/no check first turns true. - There is no array of candidates to index: the answer is a value you invent (an eating speed) and checking it means simulating the whole input, as in Koko Eating Bananas (LC 875).
- The values are not sorted, as in H-Index (LC 274). It asks the same question as H-Index II (LC 275), but the citations are not sorted. There is no order to binary search, and sorting first costs more than counting the papers for each citation value.
Earlier template
| 1 | from typing import Callable |
| 2 | Â |
| 3 | def binary_search_lower_bound(lo: int, hi: int, is_feasible: Callable[[int], bool]) -> int: |
| 4 | """First Bad Version / Search Insert Position: first candidate in [lo, hi) where is_feasible is True. |
| 5 | Precondition: is_feasible is monotonic (False, False, ..., True, True). |
| 6 | Returns hi when no candidate is True. N = hi - lo candidates. |
| 7 | Time: O(log N) | Space: O(1) |
| 8 | """ |
| 9 | while lo < hi: |
| 10 | mid = lo + (hi - lo) // 2 |
| 11 | if is_feasible(mid): |
| 12 | hi = mid # Preserve mid as candidate; search left |
| 13 | else: |
| 14 | lo = mid + 1 # Discard mid; first true must be strictly right |
| 15 | Â |
| 16 | return lo # lo == hi is the exact boundary |
Earlier problems
Is Subsequence
Lower Bound (First True Binary Search)
You have an array of distinct integers in sorted order and a target value. If target is already in the array, return its index; otherwise, return the index it would land at if you inserted it while keeping the array sorted.
Solve it in O(log n) time.
Lower Bound
candidate domain âž” monotonic boolean sequence [F, F, ..., T, T] âž” first True| 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 is False, and every candidate from hi on is True or does not exist, so the first True is always in [lo, hi].is_feasible is False before the answer and True from the answer on, so one call at mid decides the move, which is hi = mid after a True and lo = mid + 1 after a False. A True mid may be the first True, so hi = mid keeps it in the range. A False mid is not the answer and the first True is to its right, so lo = mid + 1 drops it. After either move the first True is still in [lo, hi]. hi is allowed to be the answer, because it starts one past the last candidate, so a row with no True returns hi. The range gets smaller on every pass and lo never passes hi, so the loop stops with lo == hi, and lo is the answer. The trap is lo = mid in the False branch. The midpoint is rounded down, so when hi == lo + 1, mid equals lo. Then lo does not move, and the loop never ends.
See it run
Lower Bound, 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 candidateanswerthe first True, or hi when no index is True
Start: lo = 0 and hi = 6. The answer is somewhere from lo to hi, and hi is never tested.
Gotcha 1 of 7
- The trap
Gotcha 1: Writing
lo = midinstead oflo = mid + 1Mistakelo = midFixlo = mid + 1What happens: When one candidate is left (
hiis equal tolo + 1),midequalslo. If the check is False there,lo = midleaveslowhere it was. The range never gets smaller, so the loop never ends.Example: Search Insert Position (LC 35): nums = [1, 3, 5, 6], target = 5Mistake: the loop never endsFix: returns 2
- You only need to know whether
targetis present and any one index of it, as in Binary Search (LC 704), not where a yes/no check first becomes true. See Exact Match - There is no array of candidates to index: the answer is a value you choose (an eating speed) and checking it means simulating the whole input, as in Koko Eating Bananas (LC 875). See Feasible Function
- The question asks for the largest value that still passes a check, as in Sqrt(x) (LC 69), the largest
kwithk * k <= x: the answer is the last True, not the first. See Upper Bound - The values are not sorted, as in H-Index (LC 274). H-Index II (LC 275) has sorted values and fits this card. With unsorted values there is no order to search, and sorting first costs more than counting. See Counting Sort
Theory
The question we build
What we know
nums is sorted, so nums[i] never gets smaller when i gets larger.
One comparison at the middle index therefore shows which side can still hold the answer.
What the problem gives us
The input is nums and target.
The loop needs two things built from them: a check for one index, and the range of candidate indices.
The question we build
We ask each index one yes-or-no question: "is nums[i] at least target?"
The check at_least_target(i) returns nums[i] >= target.
We use nums = [1, 3, 5, 6] and target = 5 (the first example of Search Insert Position (LC 35)).
The answers are False, False, True, True.
The values only get larger. So after one True, every later answer is True too.
The first True is index 2, so target = 5 goes at index 2.
Narrowing down to the template
Every index before the insert position holds a value smaller than target.
The insert position holds a value that is at least target.
So the insert position is the first True of at_least_target.
The candidates are the indices 0 to len(nums) - 1.
So the call is binary_search_lower_bound(0, len(nums), at_least_target).
Step 1: Linear scan
The answer is the first index where the check is True. The scan finds it by checking the indices in order. It is correct, and it states in one line what the answer means.
| 1 | def first_true_scan(lo, hi, is_feasible): |
| 2 | i = lo |
| 3 | while i < hi and not is_feasible(i): |
| 4 | i += 1 |
| 5 | return i # hi when no candidate is True |
Cost: O(N) checks, where N is the number of candidates.
Step 2: Halving loop
The scan checks every candidate.
The halving loop checks only mid, the middle of the range.
The row of answers is False…False True…True, so one check at mid shows which side of mid cannot hold the first True.
The loop discards that side, and the range is about half as long after every pass.
| 1 | def first_true_halving(lo, hi, is_feasible): |
| 2 | while lo < hi: |
| 3 | mid = lo + (hi - lo) // 2 |
| 4 | if is_feasible(mid): |
| 5 | hi = mid # mid may be the first True: keep it |
| 6 | else: |
| 7 | lo = mid + 1 # mid is False: the first True is to the right |
| 8 | return lo |
Cost: O(log N) checks.
Step 3: The named template
The loop does not change between problems. Only the check and the range change.
| 1 | New line: from typing import Callable |
| 2 | |
| 3 | New line: def binary_search_lower_bound(lo: int, hi: int, is_feasible: Callable[[int], bool]) -> int: |
| 4 | New line: """First candidate in [lo, hi) where is_feasible is True. |
| 5 | New line: Precondition: is_feasible is monotonic (False, False, ..., True, True). |
| 6 | New line: Returns hi when no candidate is True. N = hi - lo candidates. T = the cost of one is_feasible call. |
| 7 | New line: Time: O(T * log N) | Space: O(1) |
| 8 | New line: """ |
| 9 | while lo < hi: |
| 10 | mid = lo + (hi - lo) // 2 |
| 11 | |
| 12 | New line: # 1. Check the Midpoint: |
| 13 | New line: # is_feasible(mid) shows which side of the flip mid is on: before the answer (False), |
| 14 | New line: # or at the answer or after it (True). |
| 15 | if is_feasible(mid): |
| 16 | New line: # 2. Keep the Candidate: |
| 17 | New line: # mid may be the first True, so keep it in the range by moving hi down to mid. |
| 18 | hi = mid |
| 19 | else: |
| 20 | New line: # 3. Discard the Candidate: |
| 21 | New line: # mid is False, so the first True must be strictly to the right of mid. |
| 22 | lo = mid + 1 |
| 23 | |
| 24 | New line: # 4. Return the Boundary: |
| 25 | New line: # The loop stops when lo equals hi; that position is the first True, or hi when none was True. |
| 26 | return lo |
Cost: O(T * log N), where T is the cost of one is_feasible call.
| Before | After | What changed |
|---|---|---|
def first_true_halving(lo, hi, is_feasible): | def binary_search_lower_bound(lo: int, hi: int, is_feasible: Callable[[int], bool]) -> int: | A name and types. The loop is the same. |
| the loop body | the loop body | Identical. Only comments change. |
| (none) | the docstring | The contract is written down. |
The shorter version
1. A range of candidates, range(lo, hi)
| 1 | from bisect import bisect_left |
| 2 | Â |
| 3 | # is_feasible must read False, then True. |
| 4 | first = bisect_left(range(lo, hi), True, key=is_feasible) + lo |
bisect_left is Python's own version of this search. The key is_feasible turns each candidate into False or True, and bisect finds where True first appears. Bisect returns a position counted from 0 in the range, so add lo to get the candidate. For lo = 2, hi = 5 and a first True at candidate 3, bisect returns 1, and 1 + 2 = 3. When no candidate is True, the result is hi. An empty range also returns hi. Negative candidate values are allowed, because the position that bisect returns is never negative. Do not pass a negative lo to bisect itself: bisect_left(range(n), True, -1, hi, key=...) raises ValueError and does not count from the end. range does not build a list. The key= argument needs Python 3.10 or later.
2. A sorted list, nums
| 1 | from bisect import bisect_left |
| 2 | Â |
| 3 | # nums is sorted; search indices in [lo, hi). |
| 4 | insert_at = bisect_left(nums, target, lo, hi) |
| 5 | Â |
| 6 | # Records are sorted by their numeric first field. |
| 7 | records = [(1, 'Ada'), (2, 'Bo'), (2, 'Cy'), (4, 'Di')] |
| 8 | first_record = bisect_left(records, 2, 1, 4, key=lambda record: record[0]) # 1 |
bisect_left(nums, target, lo, hi) finds the first index in [lo, hi) where nums[i] >= target. The arguments are the list, the target, then the bounds. The result is already an index in nums, so do not add lo. If the target occurs in the window, the result is its first occurrence. Otherwise it is the insert position, and when no value qualifies it is hi. Indices start at 0, so for the whole list the result also equals the number of values below target. For nums = [1, 2, 2, 2, 4] and target = 2, the whole list gives 1, and the window lo = 2, hi = 5 gives 2. Check insert_at < hi before you read nums[insert_at].
For records, key=lambda record: record[0] reads the numeric field, and the records must already be sorted by that field. Bisect applies the key to the records and not to the target, so the target 2 is already a number. The bounds 1, 4 search the records at indices 1, 2 and 3, and the first record with a key of at least 2 is at index 1. Before you index, check first_record < 4.
The range [lo, hi) and what hi means
The range [lo, hi) is called a half-open range. It includes lo and does not include hi.
A closed range, written [lo, hi], includes both ends.
In [lo, hi), hi is one past the last candidate, and the search never checks hi.
Here are the candidates for nums = [1, 3, 5, 6]:
| Index | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| Value | 1 | 3 | 5 | 6 | none |
| Role | candidate | candidate | candidate | candidate | hi, one past the last candidate |
Because hi is never checked, it can stand for "no candidate is True".
For Search Insert Position (LC 35), that answer means "insert at the end".
Many books write the same search with a closed range: hi = len(nums) - 1 and while lo <= hi.
That loop is in the "Old ways" tab.
The values you pass for lo and hi depend on what you look for.
The template keeps one shape:
| What you look for | lo (first candidate) | hi (one past the last candidate) |
|---|---|---|
The first index with nums[i] >= target | 0 | len(nums) |
The first bad version, with versions 1 to n | 1 | n + 1 |
How to build is_feasible
The check is the only part that changes between problems. Ask four questions, in this order:
- What does True mean? Name it in words, for example
at_least_target. - Is the row
False…False True…True? Draw the row on a tiny input. If the row has another shape, the check is wrong. - Can I evaluate the check for one proposed candidate using the input? State the work one evaluation performs. It may read neighbours, count values, or simulate a process.
- What range
[lo, hi)holds every possible answer?hiis one past the last candidate. It is the answer when no candidate is True.
Here are three worked checks, each drawn on a tiny input.
The first two use nums = [1, 3, 5, 6] and target = 3.
nums[i] >= target is True from the first value that is at least target.
nums[i] > target is True from the first value that is larger than target.
First Bad Version (LC 278) with n = 5. Version 4 is the first bad version. The candidates are the versions 1 to n, so lo = 1 and hi = n + 1.
The contract
| Item | Lower Bound |
|---|---|
| Range | Half-open [lo, hi). hi is one past the last candidate. |
Is hi checked? | Never. mid < hi on every pass. |
| The check | Takes one candidate. Returns True or False. Must be False…False True…True over [lo, hi). |
| Returns | The first True. hi when no candidate is True. |
| Equal values | The first of them, because a True mid is kept and the search continues to the left. |
| Cost | O(T * log N) time and O(1) space. N = hi - lo, and T is the cost of one check. |
A target smaller than every value needs no special case.
Every check reads True, so each pass sets hi = mid. hi reaches lo, and the loop returns 0.
The midpoint is lo + (hi - lo) // 2. It gives the same number as (lo + hi) // 2.
In Java, C++ and C#, lo + hi can be larger than the largest 32-bit integer, and the result then becomes negative.
lo + (hi - lo) // 2 never goes above hi, so it is safe in every language. Python integers have no limit.
Real-world use
This search has standard-library names in several languages. Python's bisect.bisect_left runs this same half-open loop, and C++'s std::lower_bound returns the same position. Both find where a value belongs in a sorted sequence, for example to keep a list sorted while inserting.
Say it in an interview
About 60 seconds, for Search Insert Position (LC 35):
"I use Lower Bound, because the check
nums[i] >= targetis False, then True, so the insert position is the first True. I search[0, len(nums)), solen(nums)can mean insert at the end. A passingmidmay be the answer, sohi = mid, and a failingmidis not, solo = mid + 1. It takesO(log N)time andO(1)space. The trap islo = mid. Whenhi == lo + 1,midequalsloand the loop never ends."
Common Questions
- How do you count how many times
targetappears? Call Lower Bound twice: once with
nums[i] >= targetfor the first index, and once withnums[i] > targetfor the first index after the equal values. The difference between the two results is the count. Fornums = [1, 2, 2, 2, 4]andtarget = 2, the results are 1 and 4, so the count is 3.- How do you find the last index with
nums[i] <= targetusing Lower Bound? Find the first index where
nums[i] > target, then subtract 1. Fornums = [1, 2, 2, 2, 4]andtarget = 2, the first index withnums[i] > 2is 4, so the answer is 3. When every value is greater thantarget, the first index is 0 and the answer is -1. Upper Bound returns the same value with one call.- Why does the loop not return when
nums[mid] == target? Equal values can repeat, and the answer is the first of them. A
midthat equalstargetmay have an equal value on its left, so the loop keepsmidwithhi = midand continues to the left. Fornums = [2, 2, 2]andtarget = 2, the result is 0. An early return at the middle would give 1.
Practice
Most courses teach a new binary-search loop for every problem. This course teaches one loop, and each problem only changes the check that the loop calls.
The classic loop
| 1 | from typing import List |
| 2 | Â |
| 3 | def searchInsert(nums: List[int], target: int) -> int: |
| 4 | """Search Insert Position, the classic closed loop. |
| 5 | Time: O(log N) | Space: O(1) |
| 6 | """ |
| 7 | lo, hi = 0, len(nums) - 1 |
| 8 | while lo <= hi: |
| 9 | mid = lo + (hi - lo) // 2 |
| 10 | if nums[mid] >= target: |
| 11 | hi = mid - 1 |
| 12 | else: |
| 13 | lo = mid + 1 |
| 14 | return lo |
| 1 | def search_insert_recorded(nums, target, ans): |
| 2 | lo, hi = 0, len(nums) - 1 |
| 3 | while lo <= hi: |
| 4 | mid = lo + (hi - lo) // 2 |
| 5 | if nums[mid] >= target: |
| 6 | ans = mid |
| 7 | hi = mid - 1 |
| 8 | else: |
| 9 | lo = mid + 1 |
| 10 | return ans |
From the classic loop to our template
| Old Way line | In our template | Why |
|---|---|---|
| lo, hi = 0, len(nums) - 1 | binary_search_lower_bound(0, len(nums), at_least_target) | The template's hi is one past the last index, so "no value passes" is the answer hi. |
| while lo <= hi: | while lo < hi: | The half-open range is empty when lo is equal to hi. |
| if nums[mid] >= target: | at_least_target(i): return nums[i] >= target | The question moves into a named check. |
| hi = mid - 1 | hi = mid | The closed loop drops mid. In the template, mid can be the answer, so hi = mid keeps it, and no recorded answer is needed. |
| lo = mid + 1 | lo = mid + 1 | The same line in both forms. |
| return lo | insert_at = ..., then return insert_at | The result of the template call gets its own name. |
When the classic form is fine
Use it when an interviewer asks for this form, or when the code base already uses it. The number of steps is about the same.
A second common form keeps the answer in a variable. Write ans = len(nums) before the loop, set ans = mid just before hi = mid - 1, and return ans. It is the same search.
Gotchas of the classic loop
Gotcha 1 of 3
- The trap
Gotcha 1: Writing
hi = midin the closed loopMistakehi = midFixhi = mid - 1What happens: The range
[lo, hi]is closed, somidis checked and then dropped. Withhi = mid, the range keepsmid. Whenlo == hiand the check is True,hidoes not move.lo <= histays True, so the loop never ends.Example: nums = [1, 3, 5, 6], target = 5Mistake: the loop never endsFix: returns 2
Earlier description
is_feasible is False for every candidate before the answer and True from the answer on, so mid only tells you which side of the flip you are on. If is_feasible(mid), mid may be the first True, so keep it with hi = mid; otherwise the first True is strictly to the right, so lo = mid + 1. The first True never leaves [lo, hi] (it is hi itself when nothing is True, since hi starts one past the last candidate), and the range shrinks on every step, so the loop stops at lo == hi on the answer. The trap is writing lo = mid in the False branch: with the floor midpoint, mid == lo when two candidates are left, so lo never moves and the loop never ends.
Earlier signals
The question is 'where does something first become true': insert position, first bad version, first value at least target, how many values are below a threshold.
Earlier invariant
Every candidate below lo is False and every candidate from hi on is True (or past the end), so the first True is always in [lo, hi].
Earlier gotchas
Reading nums[i] without checking i < len(nums)
The search returns a position, not an index that is known to be valid. When no value passes the check, it returns len(nums). Reading nums[len(nums)] raises an IndexError. Find First and Last Position of Element in Sorted Array (LC 34) needs this check before it returns the first position.
Earlier look-alikes
- You only need to know whether
targetis present and any one index of it, as in Binary Search (LC 704), not where a yes/no check first turns true. - There is no array of candidates to index: the answer is a value you invent (an eating speed) and checking it means simulating the whole input, as in Koko Eating Bananas (LC 875).
- The values are not sorted, as in H-Index (LC 274). It asks the same question as H-Index II (LC 275), but the citations are not sorted. There is no order to binary search, and sorting first costs more than counting the papers for each citation value.
Earlier template
| 1 | from typing import Callable |
| 2 | Â |
| 3 | def binary_search_lower_bound(lo: int, hi: int, is_feasible: Callable[[int], bool]) -> int: |
| 4 | """First Bad Version / Search Insert Position: first candidate in [lo, hi) where is_feasible is True. |
| 5 | Precondition: is_feasible is monotonic (False, False, ..., True, True). |
| 6 | Returns hi when no candidate is True. N = hi - lo candidates. |
| 7 | Time: O(log N) | Space: O(1) |
| 8 | """ |
| 9 | while lo < hi: |
| 10 | mid = lo + (hi - lo) // 2 |
| 11 | if is_feasible(mid): |
| 12 | hi = mid # Preserve mid as candidate; search left |
| 13 | else: |
| 14 | lo = mid + 1 # Discard mid; first true must be strictly right |
| 15 | Â |
| 16 | return lo # lo == hi is the exact boundary |
Earlier problems
Is Subsequence