Coding Interview Patterns Module 9 – Binary search
First, last and count with bounds
Find the first and last position of a value in sorted data, and count the values in a range, with two binary-search bounds per question.
What you will learn
- Find the first and last positions of a value in a sorted list with two bounds
- Count the values that lie in a range [a, b] as the difference of two bounds
- Write both bounds in JavaScript and Java, and use the library bounds of Python, C++ and Go
- Choose between bounds per question, a counting table and one sorted walk for many questions
Before you start
What you’ll learn · Before you start
What you will learn
- Find the first and last positions of a value in a sorted list with two bounds
- Count the values that lie in a range [a, b] as the difference of two bounds
- Write both bounds in JavaScript and Java, and use the library bounds of Python, C++ and Go
- Choose between bounds per question, a counting table and one sorted walk for many questions
Before you start
On this page
In a sorted list, every copy of a value sits next to the others, so two binary searches describe all of them. The lower bound of x is where the copies start: the first position whose value is at least x. The upper bound of x is where they stop: the first position whose value is greater than x. The first copy is at the lower bound, the last copy just before the upper bound, and their number is the difference of the two. A range of values works the same way: the values from a to b fill the positions from the lower bound of a up to the upper bound of b. Each question then costs two searches of about log₂ n steps, however many copies there are, where a scan reads all n values. To count with bounds, subtract two positions. This lesson states an original problem with up to 100,000 such questions, measures the scan that is too slow for it, and works through the two bounds on an example.
The problem: questions about a sorted log
The problem
A payment service writes one line to its log for every transaction, tagged with the second of the day in which it happened, a whole number from 0 to 86,399. Lines are written in time order, so the seconds are sorted, and a busy second holds many lines. Support staff ask q questions of two kinds. For a second t: the position of its first line, the position of its last line and how many lines it has, or (−1, −1, 0) when it has none. For a window [a, b]: how many lines fall inside it, both ends included. Limits: up to 1,000,000 lines and 100,000 questions, with 0 ≤ a ≤ b ≤ 86,399.
Three examples on the log [61, 61, 62, 64, 64, 64, 64, 70, 95, 95], which every version of the code in this lesson
answers:
- Second 64 gives
(3, 6, 4): its four lines are lines 3 to 6. - Second 63 gives
(−1, −1, 0): nothing happened in that second. - The window [62, 70] gives 6, the window [71, 94] gives 0, and the window [0, 86,399] gives all 10 lines.
The brute force: read the whole log for every question
For a second, walk the log, remember the first and the last line that match and count the matches; for a window, count the lines whose second lies between a and b. It is plainly correct, which makes it the judge for faster code, but every question reads every line, so q questions cost O(n × q): 10¹¹ line reads at the limits.
"""First line, last line and count of a second in a sorted log, by scanning every line: the brute force."""
import random
import time
def scan_second(seconds, t):
"""(first, last, count) of the lines logged in second t, or (-1, -1, 0)."""
first, last, count = -1, -1, 0
for i, s in enumerate(seconds):
if s == t:
if first == -1:
first = i
last = i
count += 1
return first, last, count
def scan_window(seconds, a, b):
"""How many lines were logged from second a to second b, both included."""
return sum(1 for s in seconds if a <= s <= b)
log = [61, 61, 62, 64, 64, 64, 64, 70, 95, 95]
print(scan_second(log, 64), scan_second(log, 63))
print(scan_window(log, 62, 70), scan_window(log, 71, 94), scan_window(log, 0, 86_399))
# A busy day: 1,000,000 lines, one second of the day (0 to 86,399) each, sorted as a log is.
rng = random.Random(7)
day = sorted(rng.randrange(86_400) for _ in range(1_000_000))
start = time.perf_counter()
for t in (0, 43_200, 86_399):
scan_second(day, t)
per_question = (time.perf_counter() - start) / 3
print(f"one question reads all 1,000,000 lines: about {per_question * 1000:,.0f} ms")
print(f"100,000 questions: about {per_question * 100_000 / 60:,.0f} minutes") Output
(3, 6, 4) (-1, -1, 0) 6 0 10 one question reads all 1,000,000 lines: about 418 ms 100,000 questions: about 697 minutes
This output changes from run to run: the times depend on the computer and on what else it is doing
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 scan_log.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
In the recorded run one question took a few hundred milliseconds, so 100,000 questions would take many hours. Support staff want their answers while the customer is still on the phone.
Python Online Compiler Paste the scan, time it on a log of your own, then time two bisect calls on the same log.What to notice
The log is sorted, so the lines of one second form a single unbroken block, and so do the lines of any window. A block is fixed by two positions, where it starts and where it stops, and both are bounds. Only those two positions need a search; the lines between them never have to be read.
Two bounds for every question
For a second t, search the log twice, with the loop of the lesson on binary search invariants:
lo = lower_bound(log, t), the first line whose second is t or later;hi = upper_bound(log, t), the first line whose second is after t.
If lo == hi the block is empty and the answer is (−1, −1, 0). Otherwise the first line is lo, the last line is
hi − 1, and the count is hi − lo. Windows count with bounds the same way: the lines of [a, b] start at
lower_bound(a) and stop before upper_bound(b), so the window holds upper_bound(b) − lower_bound(a) lines.
Two bounds fix a block of the sorted log
Text description of the diagram
The diagram shows the sorted log 61, 61, 62, 64, 64, 64, 64, 70, 95, 95 as ten cells, with the positions 0 to 9 under them. The four cells holding 64, at positions 3 to 6, are highlighted.
An arrow labelled "lower bound 3" points at the left edge of position 3, the first line whose second is 64 or later. An arrow labelled "upper bound 7" points at the left edge of position 7, the first line whose second is after 64. A bracket under positions 3 to 6 reads "second 64: 7 − 3 = 4 lines".
A second, wider bracket runs from position 2 to position 7 and reads "window [62, 70]: 8 − 2 = 6 lines": the lower bound of 62 is 2 and the upper bound of 70 is 8. The note at the bottom says that blocks start at a lower bound and end just before an upper bound.
Each bound halves the unknown part of the log at every step, so it needs at most ⌊log₂ n⌋ + 1 steps: 20 for a million lines. Two bounds per question and 100,000 questions come to at most 4 million steps, where the scans needed 10¹¹ line reads.
A worked example
The two searches for second 64 on the example log, with lo = 0 and hi = 10 at the start:
| Step | Lower bound of 64: middle, its second, then | Upper bound of 64: middle, its second, then |
|---|---|---|
| 1 | 5 holds 64, not below 64: hi = 5 |
5 holds 64, not after 64: lo = 6 |
| 2 | 2 holds 62, below 64: lo = 3 |
8 holds 95, after 64: hi = 8 |
| 3 | 4 holds 64: hi = 4 |
7 holds 70, after 64: hi = 7 |
| 4 | 3 holds 64: hi = 3 |
6 holds 64: lo = 7 |
| End | lo == hi == 3 |
lo == hi == 7 |
The block of second 64 is therefore lines 3 to 6, and 7 − 3 = 4 lines. Notice step 1 of the lower bound: it lands on a 64 and keeps going left, because an earlier 64 could still start the block. A search that stopped at the first 64 it met would report line 5 as the first line.
Check yourself
4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
The two bounds in five languages, proved and stress-tested Pro: opens with a Pro, Premium or Ultimate pass
Python and C++ ship both bounds, Go ships one of them and a general search, and Java and JavaScript need them written out. Here are the five versions with their recorded runs, the argument that the block between the bounds holds exactly the lines of one second, and four one-line slips that a stress test catches at once.
In this part:
- Python, JavaScript, Java, C++ and Go on the same log
- Why the block between the bounds is exactly the lines of second t
- Four slips, caught by a stress test against the scans
3 runnable examples (C++, Go, Java, JavaScript, Python) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Library calls that look logarithmic but scan, measured Pro: opens with a Pro, Premium or Ultimate pass
A short call name can hide a scan. This part measures one in C++ on a second with a million lines, shows how Python searches whole log records by one of their fields, and sets out which call gives which bound in each language.
In this part:
- A million lines in one second: three ways to count them in C++
- Searching log records by their second with key=
- Which call gives which bound, in five languages
2 runnable examples (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
A hundred thousand windows: bounds, a table or one sorted walk Pro: opens with a Pro, Premium or Ultimate pass
When all the questions arrive together, two bounds per question is one of three good answers. This part times the three on the same log of a million lines, sets out when each one wins, and ends with the edge cases, variants, follow-up answers and an exercise.
In this part:
- Measured: three answers to 100,000 windows
- The trade-offs side by side
- Edge cases and common bugs
- Variants, with answers
- Follow-up questions, answered
- Practise: count the orders in each price band
- Count the orders in each price band
- Results of the sample tests
- Key takeaways from the depth
1 runnable example (Python) · 1 exercise with sample tests · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
Which two bounds count the values of a sorted list that lie in a closed range [a, b], and which two count them in a half-open range [a, b)?
Both counts start at the lower bound of a, the first position whose value is at least a, so every copy of a is counted. They differ only at the right end. A closed range keeps the copies of b, so it stops at the upper bound of b, the first value greater than b, and the count is upper_bound(b) − lower_bound(a). A half-open range leaves b out, so it stops at the lower bound of b, the first value that is b or more: lower_bound(b) − lower_bound(a). On [61, 61, 62, 64, 64, 64, 64, 70, 95, 95] the closed range [62, 70] holds 8 − 2 = 6 values and the half-open range [62, 70) holds 7 − 2 = 5, because the 70 drops out. In Python the two bounds are bisect_left and bisect_right.
-
Mid to senior
A sorted log holds 5 × 10⁷ event times from one day, in milliseconds. You must answer 10⁶ questions of the form "how many events between t1 and t2?" in about a second of CPU time. Compare two bounds per question, a counting table and sorting the questions, with time and memory figures, and choose.
-
Mid to senior
Values arrive one at a time, and between arrivals you are asked how many values so far lie in [a, b]. There are 2 × 10⁵ arrivals and 2 × 10⁵ questions, with values up to 10⁹. Why is a sorted list with insertion too slow, and what do you use instead?
Key takeaways
- In sorted data the copies of a value form one block: it starts at the lower bound and stops just before the upper bound.
- The first copy is at the lower bound, the last at the upper bound minus one, and their count is the difference; equal bounds mean the value is absent.
- A range [a, b] holds
upper_bound(b) − lower_bound(a)values, so each question costs two searches of about log₂ n steps. - On a million lines that is 40 steps a question, against a scan that took hundreds of milliseconds each.
References
- bisect: array bisection algorithm (Python Software Foundation)
- itertools.accumulate (Python Software Foundation)
- Java SE 25 API: Arrays.binarySearch (Oracle)
- std::equal_range (cppreference.com)
- std::lower_bound (cppreference.com)
- std::multiset::count (cppreference.com)
- Package slices: BinarySearch (The Go Authors)
- Package sort: Search and SearchInts (The Go Authors)
- ECMAScript language specification: properties of the Array prototype object (Ecma International (TC39))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress