LeetCode #34 Medium

Find First and Last Position of Element in Sorted Array

Find First and Last Position of Element in Sorted Array: return the starting and ending indices of a target value in a sorted array, or [-1, -1] if it is absent, in O(log n) time.

Constraints
  • 0 <= nums.length <= 10⁵
  • -10⁹ <= nums[i] <= 10⁹
  • nums is a non-decreasing array
  • -10⁹ <= target <= 10⁹
arraybinary search
Open on LeetCode ↗
Find First and Last Position of Element in Sorted Array diagramA labelled diagram of the structure this problem turns on.two boundary searches bracket the run — it is never traversed5778810idx 0idx 1idx 2idx 3idx 4idx 5lower_bound(8) = 3lower_bound(9) = 5answer = [3, 5 − 1] = [3, 4]expanding outward from a match instead would be O(n) on an all-equal array
02

Intuition

A plain binary search finds some occurrence, but which one it lands on is arbitrary, and expanding outward from it costs O(n) when the whole array is the target. The fix is to run two boundary searches instead: one that finds the leftmost position where the target could sit, and one that finds the position just past its last occurrence. Two O(log n) passes give both ends without ever scanning the run.

How to spot this pattern

When a sorted array holds duplicates and a question asks about the extent of a value — its first index, last index, or count — the answer is two boundary searches. Counting occurrences is the same pair subtracted, which is the whole of Count of Occurrences.

03

Approach

Try it first

Before reading on: work out why expanding outward from a found index breaks the time bound, and construct the input where it is worst. Then express the last occurrence in terms of a lower-bound search rather than a separate upper-bound one.

1

Why the naive find-then-expand fails the bound

After locating any occurrence, walking left and right to the edges of the run is correct but linear in the run's length. On an array of ten thousand identical values the expansion touches every element, so the total is O(n) and the required logarithmic bound is missed. The run's length is exactly what must not be traversed, which forces the boundaries themselves to be searched rather than discovered by scanning.

2

Two boundaries from one helper

Define lower_bound(x) as the first index whose value is at least x. Then the first occurrence of the target is lower_bound(target), and the index one past its last occurrence is lower_bound(target + 1) — because the first element exceeding the target begins immediately after the run ends. So the answer is [lo, hi - 1] where lo = lower_bound(target) and hi = lower_bound(target + 1). Writing one helper twice is far less error-prone than writing two subtly different searches, which is where most attempts introduce bugs.

3

Detecting absence, and the half-open interval

The target is missing precisely when lo == hi, meaning no element lies in [target, target + 1) — an empty run. That single test covers every absent case, including a target smaller than everything, larger than everything, or falling in a gap, so no separate bounds checks are needed. The helper itself uses lo = 0, hi = len(nums) with while lo < hi and hi = mid, keeping the interval half-open so that an answer of len(nums) remains representable. Two searches give O(log n) total with O(1) space.

04

Solution & live demo

1class Solution:
2 def searchRange(self, nums, target):
3 def lower_bound(x):
4 lo, hi = 0, len(nums)
5 while lo < hi:
6 mid = (lo + hi) // 2
7 if nums[mid] < x:
8 lo = mid + 1
9 else:
10 hi = mid
11 return lo
12 
13 start = lower_bound(target)
14 end = lower_bound(target + 1)
15 if start == end:
16 return [-1, -1]
17 return [start, end - 1]
05

Common pitfalls

Expanding outward after finding a match

✗ Wrong
while nums[i - 1] == target:
    i -= 1
✓ Right
start = lower_bound(target)

The expansion is linear in the run's length, so an array of identical values makes it O(n) and the required O(log n) is missed on exactly the input the problem is testing for.

Using hi = mid - 1 in the lower-bound helper

✗ Wrong
else:
    hi = mid - 1
✓ Right
else:
    hi = mid

When nums[mid] equals the target, mid may itself be the first occurrence. Excluding it discards the answer and the search settles one position too far left.

Reporting absence with a bounds check on lo

✗ Wrong
if lo >= len(nums) or nums[lo] != target:
    return [-1, -1]
✓ Right
if start == end:
    return [-1, -1]

The bounds version works but duplicates logic the second search already computed. Comparing the two boundaries covers absence, an empty array, and out-of-range targets with one test and no indexing.

06

Edge cases

Target absent from a non-empty array

lo equals hi, so [-1, -1] is returned.

Empty array

Both searches return 0, they are equal, and [-1, -1] follows.

Every element is the target

lo is 0 and hi is n, giving [0, n - 1] without scanning the run.

Single occurrence

hi is exactly lo + 1, so the range collapses to one index.

Target larger than every element

Both bounds land at len(nums) and are equal, so absence is reported.

07

Complexity

Time
O(log n)
Space
O(1)
Two independent boundary searches, each halving the range. The run itself is never traversed.