bigonotes
DSA: Patterns & PythonBinary Search

Binary Search Boundaries

Exact match, first and last occurrence, floor and ceiling: pick the loop condition and read the answer from where the pointers stop.

6 min readLast updated

When the input is sorted (or rotated sorted), think binary search. Each step halves the range, so a search over nn items takes O(log⁡n)O(\log n). Most bugs come from two decisions: the loop condition and what the pointers mean when the loop ends. This note fixes both for exact matches, first and last occurrences, and floor and ceiling.

The idea#

Choose the loop condition by what you are looking for.

Loop condition When to use it Post-loop state
l <= r Looking for a specific target; the range may be a single element The range is empty. Check l or r for the closest value
l < r Narrowing to one index: a "tipping point" or "peak" l == r. That single element is the answer

Once the condition is set, one rule of thumb decides what the loop body does:

  • Exact match: check nums[mid] == target and return.
  • Closest or threshold match: don't stop early. Let the pointers cross and read the answer from l and r. This squeezes the range down onto the boundary.

The post-loop sandwich#

Letting the pointers cross only helps if you know where they stop. With l <= r, the loop ends only when l = r + 1. If the target isn't present, the pointers have sandwiched the spot where it would be:

  • r sits on the largest value smaller than the target (the floor).
  • l sits on the smallest value larger than the target (the ceiling), which is also the insertion index.
After an l <= r binary search for 6 in 1 3 5 7 9 fails, r rests on 5 (the floor) and l rests on 7 (the ceiling and insertion index)

This works because of an invariant: everything left of l is smaller than the target, and everything right of r is larger. Each step keeps that true, so when the range is empty, r and l are the two neighbors of the gap.

Record and keep going#

The sandwich handles a missing target. Duplicates need one more idea. For the first occurrence, a hit at mid is only a candidate: there may be an earlier one. Note it down and keep searching left. For the last occurrence, note it and search right.

How to spot it#

These cues point to a boundary search.

  • Sorted array plus "first", "last", "insert position", "smallest greater than", "largest at most".
  • "Return the index where it would be" or "the closest value".
  • A yes/no API that flips once (bad version, guess higher or lower).
  • An integer answer to a monotone question, such as the floor of a square root.

Template#

One loop covers the exact match and the first or last occurrence. Pass a find_first flag instead of toggling shared state.

l, r = 0, len(nums) - 1
found = -1
while l <= r:
    mid = (l + r) // 2
    if nums[mid] == target:
        found = mid                  # a candidate, not yet the edge
        if find_first: r = mid - 1   # an earlier copy may exist
        else: l = mid + 1            # a later copy may exist
    elif nums[mid] < target:
        l = mid + 1
    else:
        r = mid - 1
return found

For a plain exact match, drop found and return on a hit instead. After the loop, r is the floor and l is the ceiling. Either can be off the end (-1 or len(nums)).

Worth exploring: Python's bisect module (bisect_left is the ceiling, bisect_right the first index past the last copy) and patience sorting for LIS (LC 300, 354). Both are natural next steps after boundaries.

Pitfalls#

Off-by-one errors here usually come from one of these.

  • Mixing loop styles. l <= r goes with r = mid - 1; l < r goes with r = mid. Mixing them loops forever or skips the answer.
  • Stopping on the first hit when the question asks for the first or last occurrence.
  • Reading l out of range. After the loop, l can equal len(nums) and r can be -1. Guard before indexing.
  • Overflow guards. l + (r - l) // 2 matters in fixed-width languages; Python integers don't overflow, so (l + r) // 2 is fine.

Problems in this pattern#

These problems use this pattern. Solve them before you open the hints below. Start with the first two.

Hints#

Open a hint only after you have tried the problem for a while.

Hint: Binary Search

Use l <= r and return mid on an exact hit. Return -1 once the range is empty.

Hint: Search Insert Position

Let the search fail. The answer is l, which equals r + 1.

Hint: First Bad Version

If mid is bad, everything after it is bad too. Record it and go left; otherwise go right.

Hint: Guess Number Higher or Lower

The API tells you which half to drop. It is an exact-match search where guess(mid) replaces the comparison.

Hint: Find First and Last Position of Element in Sorted Array

Run the template twice, once with find_first true and once false. For [5, 7, 7, 8, 8, 10] and target 8, you get 3 and 4.

Hint: Sqrt(x)

Search the integers 0..x. When the loop ends, r is the floor: r * r is the largest square ≤ x.

Hint: Find Smallest Letter Greater Than Target

Move right on letters[mid] <= target, so l lands on the first letter strictly greater. When l == len(letters), wrap to index 0.

Hint: Closest value to a target

After the loop, compare the candidates at r and l. Guard both ends before you index.

Hint: Sum of Mutated Array Closest to Target

Search the answer space. The loop ends with r as the largest value whose sum is at most the target and l as the smallest value whose sum is above it. Compare the two and prefer the smaller value on a tie. See Binary Search on the Answer.

Hint: Median of Two Sorted Arrays

Binary search a cut in the shorter array so that the two left halves together hold half the elements. The cut is right when each left maximum is at most the other array's right minimum. Use infinities when a side is empty.

Key takeaways#

  • l <= r for a specific target; l < r to narrow down to one index.
  • Exact match: check mid. Closest or threshold: let the pointers cross.
  • After l <= r fails, r is the floor and l is the ceiling (and the insert position).
  • For first or last occurrence, record the hit and keep searching in that direction.
  • Always guard l == len(nums) and r == -1 before indexing.
Revision notes · 2 min

Core idea#

Binary search halves a sorted range each step, so it takes O(log⁡n)O(\log n). Most bugs come from two choices: the loop condition, and what the pointers mean when the loop ends.

Key points#

  • Exact match: check nums[mid] == target and return.
  • Closest or threshold match: do not stop early. Let the pointers cross and read the answer from l and r.
  • With l <= r, the loop ends at l = r + 1.
  • If the target is missing, r is the floor and l is the ceiling. l is also the insert position.
  • Invariant: everything left of l is smaller than the target, and everything right of r is larger.
  • First or last occurrence: a hit is only a candidate. Record it and keep searching left (first) or right (last).
  • Spot it: sorted input plus "first", "last", "insert position", "closest", or a yes/no API that flips once.

Pick the loop#

Condition Use for After the loop
l <= r with r = mid - 1 a specific target range empty; check l, r
l < r with r = mid one index (tipping point, peak) l == r is the answer

Read-outs#

Problem Answer
Search insert position l
Integer square root r (floor)
Smallest letter greater than target l, wrap to 0 at len(letters)
Closest value compare r and l

Template#

l, r = 0, len(nums) - 1
found = -1
while l <= r:
    mid = (l + r) // 2
    if nums[mid] == target:
        found = mid                  # candidate, keep going
        if find_first: r = mid - 1
        else: l = mid + 1
    elif nums[mid] < target:
        l = mid + 1
    else:
        r = mid - 1
return found

Pitfalls#

  • Mixing loop styles, such as l <= r with r = mid. It loops forever or skips the answer.
  • Stopping on the first hit when the question asks for the first or last occurrence.
  • Indexing out of range. After the loop, l can be len(nums) and r can be -1.
  • Overflow guards are not needed in Python. (l + r) // 2 is fine.

Say it in an interview#

  • "l <= r for a target, l < r to narrow to one index."
  • "When the target is missing, r is the floor and l is the ceiling."

Found a mistake? Email ask@bigonotes.com