Floor and Ceiling in a Sorted Array is a classic binary-search warm-up. It takes the plain "find this value" search and bends it into "find the closest value on each side" — the move behind range queries, autocomplete bounds, and price-tier lookups.
Problem. Given a sorted array a and a value x, return its floor (the largest element
that is <= x) and its ceiling (the smallest element that is >= x). If no such element exists on a
side, that answer is None.
Example: a = [1, 2, 8, 10, 12, 19], x = 5 → floor 2 (largest value <= 5) and ceil 8 (smallest value >= 5).
The slow way first
The obvious idea: scan the whole array and keep the best candidate on each side. For every element, if it is <= x and bigger than the floor so far, it becomes the new floor; if it is >= x and smaller than the ceil so far, it becomes the new ceil. That works, but it touches every element — O(n) — and completely ignores the gift the problem handed us: the array is sorted.
The question to ask: the array is sorted, so can I jump instead of crawl? Yes — binary search lets us halve the search space each step and land on both answers in O(log n).
The idea: one binary search that records both
Run a single binary-search pass. At the middle element a[mid], only two cases matter:
- If
a[mid] <= x, it is a valid floor candidate. Save it, then go right — a larger value that is still<= xcan only be further right. - If
a[mid] >= x(here, strictly greater), it is a valid ceil candidate. Save it, then go left — a smaller value that is still>= xcan only be further left.
The key insight: every time we move, we overwrite the candidate with a strictly better one. Whatever value sits in floor and ceil when the window empties out is the answer — no extra comparison needed.
Walk through it
Step through the animation. The pointer mid lands in the current window, we compare a[mid] to 5, and based on the result we record one answer and discard half the array (dimmed). Watch the two labels: floor only ever grows toward x from below, and ceil only ever shrinks toward x from above. When lo passes hi, the labels hold 2 and 8.
Pseudocode
floor = ceil = None
lo, hi = 0, last index
while lo <= hi:
mid = (lo + hi) // 2
if a[mid] <= x:
floor = a[mid] # valid floor, maybe a bigger one is to the right
lo = mid + 1
else:
ceil = a[mid] # valid ceil, maybe a smaller one is to the left
hi = mid - 1
return floor, ceilThe Python solution
def floor_ceil(a, x):
floor = ceil = None
lo, hi = 0, len(a) - 1
while lo <= hi:
mid = (lo + hi) // 2
if a[mid] <= x:
floor = a[mid]
lo = mid + 1
else:
ceil = a[mid]
hi = mid - 1
return floor, ceilfloorandceilstart asNoneso a missing side staysNone(e.g.xsmaller than everything has no floor).loandhibracket the current search window; the loop runs while it is non-empty.mid = (lo + hi) // 2is the middle index of that window.- When
a[mid] <= x, line 7 records it as the floor and line 8 moves right to hunt for something bigger but still<= x. - When
a[mid] > x, line 10 records it as the ceil and line 11 moves left to hunt for something smaller but still>= x. - If
xis itself in the array, it gets caught by the<= xbranch as the floor; the ceil branch then closes in until ceil also equalsx.
Complexity
| Case | Time | Notes |
|---|---|---|
| Linear scan | O(n) (moderate) | checks every element |
| Binary search (this solution) | O(log n) (fast) | halves the window each step |
O(1) (fast)We use only a handful of variables, so the extra space is O(1). The win comes from exploiting the sorted order: O(n) becomes O(log n), and we still get both boundaries in a single pass.
When this pattern shows up
Whenever a problem says "sorted" and asks for the closest, boundary, first/last position, or "insert position," reach for binary search that records a candidate instead of returning on an exact match. The same skeleton powers floor/ceil, lower/upper bound, and search-insert-position.
Mind the equality. Putting a[mid] == x in the floor branch (via <=) makes the floor equal to x when
x is present, which is correct. If you instead need the strict predecessor or successor, change the
comparisons to < and > so an exact match is excluded from both sides.
Practice
For a = [1, 2, 8, 10, 12, 19] and x = 5, the first mid is index 2 with a[2] = 8. Which answer does this update, and which way does the search move?
1. Why does this run in O(log n) instead of O(n)?
2. When a[mid] <= x, which way does the search move and why?
3. What do floor and ceil hold when the loop ends?
4. What is the extra space used by this solution?