Data Structures · Binary Search · A Field Guide

First and last
position of a value

One binary search finds an occurrence. To pin down the edges of a run of duplicates, don't stop when you find it — keep leaning toward the boundary.

5778810
01 · Strip it down

Leftmost and rightmost index of the target

Given a sorted array and a target, return the first and last index where the target appears, or [-1,-1] if it's absent. For [5,7,7,8,8,10], target 8 → [3,4]. The hard constraint is O(log n), which rules out a linear scan and points straight at binary search.

Why plain binary search falls short Normal binary search returns any one occurrence — it might land on index 3 or 4. With duplicates, one hit doesn't tell you where the run begins or ends. You need a boundary, not just a match.

02 · The one change

When you find it, don't stop — lean

The whole trick is a single tweak to standard binary search. When nums[mid] == target, save the index but keep searching in one direction: left for the first occurrence, right for the last.

First occurrence
save mid, go left
right = mid − 1 — there might be an earlier copy.
Last occurrence
save mid, go right
left = mid + 1 — there might be a later copy.
The trap that quietly passes small tests Returning immediately on a match gives [2,2] for [2,2,2,2,2] instead of [0,4]. And "find one, then scan outward" is O(n) in the worst case — a solid run of duplicates defeats the log-n requirement.

Found it? Keep pushing that way

Like finding a friend in a crowd and then edging left to reach the very front of their group — you don't stop at the first hello.

Finding the F→T flip

Rewrite the array as "is this ≥ target?" — it reads F F F T T T. Binary search just hunts the single place where false turns to true.

Two closing walls

Left and right pointers are walls sliding inward, halving the room each step until they pin the boundary between them.


04 · Watch it search

Step the pointers to the boundary

Pick First or Last, set a target, and step through. The left and right walls bracket the live range; mid is checked each step. On a match the index is saved (teal ring) and the search leans onward; the final boundary locks in solid teal.

Boundary binary search

target
left0
mid–
right0
saved–1

05 · The deeper view

It's really lower bound and upper bound

Reframe each occurrence as a boundary in a boolean array. The first occurrence is the first index where nums[i] ≥ target (the lower bound). One past the last occurrence is the first index where nums[i] > target (the upper bound). So last = upperBound − 1.

Above: nums[i] ≥ target for [5,7,7,8,8,10], target 8 → the first T is the answer's left edge. Flip the test to > and the first T marks one-past-the-end. A neat bonus: count = upperBound − lowerBound.


06 · The code

Two leaning searches

int[] searchRange(int[] nums, int target) {
    return new int[]{ find(nums, target, true), find(nums, target, false) };
}

int find(int[] nums, int target, boolean first) {
    int lo = 0, hi = nums.length - 1, ans = -1;
    while (lo <= hi) {
        int mid = lo + (hi - lo) / 2;         // avoids overflow
        if (nums[mid] == target) {
            ans = mid;                          // candidate — keep leaning
            if (first) hi = mid - 1; else lo = mid + 1;
        } else if (nums[mid] < target) lo = mid + 1;
        else hi = mid - 1;
    }
    return ans;
}

Two O(log n) searches run back to back — still O(log n), because sequential (not nested) log-n work just adds: 2 log n = O(log n). Space is O(1). The empty array needs no special case: lo=0, hi=−1 skips the loop and returns −1.


07 · Trace table

First occurrence of 8 in [5,7,7,8,8,10]

When binary search misbehaves, a trace table exposes it fast — log each step's window and decision:

left mid right nums[mid] Action
0 2 5 7 7 < 8 → left = 3
3 4 5 8 match → save 4, right = 3
3 3 3 8 match → save 3, right = 2

Loop ends with left > right; the last saved index, 3, is the first occurrence. The last-occurrence search is identical but leans right, landing on 4.


08 · Traps to avoid

Where binary search bites

Leaning the wrong way

First occurrence must go right = mid − 1. Writing left = mid + 1 walks away from the boundary. Memory hook: first leans left, last leans right.

Scanning outward

Find one, then walk to both ends — looks clever, but [8,8,…,8] makes it O(n). Use binary search for both edges.

Midpoint overflow

(lo+hi)/2 can overflow. Prefer lo + (hi−lo)/2 — the standard safe form.

Mismatched template

lo ≤ hi with inclusive bounds, or lo < hi with a half-open [lo,hi). Don't mix pieces of different templates.


09 · The pattern

Binary search finds boundaries, not just numbers

The real generalization: binary search locates the flip point in any monotonic search space — anywhere a condition goes F…F T…T. "Find a value in a sorted array" is just the simplest case. Once you see it as boundary-finding, a whole family opens up:

first ≥ x
lower bound
search-insert position
first > x
upper bound
last ≤ x is upperBound − 1
count of x
upper − lower
occurrences in one shot
min feasible answer
binary search on the answer
capacity, speed, threshold problems

10 · One month from now

Reconstruct it, don't memorize it

  1. Sorted array + O(log n) → binary search.
  2. Plain binary search stops at any one match — but I need an edge.
  3. On a match, save it and keep going: left for first, right for last.
  4. The last saved index when the loop ends is the boundary.
  5. Run it twice; return [-1,-1] if nothing was ever saved.
The transferable insight Binary search isn't fundamentally about finding a number — it's about locating a boundary in a monotonic space. First/last position, first feasible value, minimum valid capacity, maximum achievable speed: all the same move once you spot the F→T flip.