Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 25 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, quickjs 0.32.0, Java 25, Clang 21.0.0 and Go 1.27.1
  • By MySmartCoPilot
Free core, Pro depth: 7 of 25 minutes free
Free: The problem: questions about a sorted log · Two bounds for every question. With a pass: 3 in-depth parts and 3 interview answers, about 18 minutes.

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:

  1. Second 64 gives (3, 6, 4): its four lines are lines 3 to 6.
  2. Second 63 gives (−1, −1, 0): nothing happened in that second.
  3. 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.

Scanning the log for each question, timed on a million lines Python · brute/scan_log.py
"""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

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:

  1. lo = lower_bound(log, t), the first line whose second is t or later;
  2. 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.

A sorted log of ten seconds; the four 64s lie between lower bound 3 and upper bound 7; the window [62, 70] spans positions 2 to 7.lower bound 3upper bound 7610611622643644645646707958959second 64: 7 − 3 = 4 lineswindow [62, 70]: 8 − 2 = 6 linesBlocks start at a lower bound, end before an upper bound

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.

  1. Question 1 of 4 In the log [61, 61, 62, 64, 64, 64, 64, 70, 95, 95], what are the lower bound and the upper bound of 70?

    Choose one answer.

    Show the answer to question 1

    Answer: 7 and 8

    The lower bound is the first position whose second is 70 or later, position 7. The upper bound is the first position whose second is after 70, position 8, which holds 95. Second 70 therefore has 8 − 7 = 1 line, at position 7.

  2. Question 2 of 4 Which expression counts the lines of a sorted log whose second lies in the window [a, b], both ends included?

    Choose one answer.

    Show the answer to question 2

    Answer: upper_bound(b) − lower_bound(a)

    The block of the window starts at the first line whose second is at least a, which is lower_bound(a), and stops at the first line whose second is greater than b, which is upper_bound(b). lower_bound(b) would leave out the lines of second b, upper_bound(a) would leave out the lines of second a, and adding 1 counts one line too many.

  3. Question 3 of 4 Second t has no lines in the log. What do its lower bound and its upper bound return?

    Choose one answer.

    Show the answer to question 3

    Answer: The same position, where a line of second t would be inserted

    Both bounds count the lines before second t: no line is equal to t, so "below t" and "at most t" are the same lines. The block between them is empty, which is how the search knows to answer (−1, −1, 0).

  4. Question 4 of 4 The log has 1,000,000 lines. At most how many halving steps do the two bounds of one question take together?

    Type a number.

    Show the answer to question 4

    Answer: 40 steps

    One bound halves the unknown part of the log at every step, so it takes at most ⌊log₂ 1,000,000⌋ + 1 = 20 steps, and the two bounds take at most 40. A scan reads all 1,000,000 lines for the same answer.

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.

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.

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.

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

  1. 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.

  2. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Fresher to mid

    Your function returns the first and the last index of x in a sorted array with two binary searches. It passes the samples, but hidden tests report a wrong last index on some inputs and an index error on others. What are the likely bugs, and which small tests expose each one?

    Model answer, Pro. First step and follow-ups

  4. 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?

    Model answer, Pro. First step and follow-ups

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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.