DSA sheet · Binary Search · Lower & upper bound pattern

Floor & Ceiling in a Sorted Array

Two problems taught together because they are mirror images of each other. The floor of x is the biggest value that is still ≤ x. The ceiling is the smallest value that is still ≥ x. The teacher solves floor carefully, then shows that ceiling needs only the comparison and the two pointer moves swapped. Both use the trick from First & Last Position: when mid could be the answer, save it in ans and keep searching for a better one.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the conditions from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

Why binary search works here

The array is sorted. For floor with x = 5, ask each cell "is arr[i] <= 5?" In [1, 2, 8, 10, 11, 12, 19] the answers are yes yes no no no no no. In a sorted array these answers switch only once, and the floor is the last "yes". Binary search finds a switch point by checking the middle and discarding the half that can't contain it.

left, right, mid

Why mid = left + (right - left) // 2?

There are two ways to compute mid: (left + right) // 2, or left + (right - left) // 2. They give the same value. In Java/C++, if the range is very big, left + right can exceed the largest int and overflow (wrap to a wrong, negative number). The second form never builds a number bigger than right. The teacher's advice: always use the second form, so you never have to worry about it. Python integers can't overflow, so in Python it's only a habit, but a good one.

Lower bound vs upper bound, and where floor and ceiling land

arr = [1, 2, 8, 10, 10, 12, 19], x = 5 (not in the array):

index0123456
value12810101219
boundsfloor↑LB=UBfloor = UB − 1 = 1 (value 2), ceil = LB = 2 (value 8)

Same array, x = 10 (present twice):

index0123456
value12810101219
bounds↑LB↑UBceil = LB = 3 (first 10), floor = UB − 1 = 4 (last 10)
  x = 5:   1   2 | 8   10  10  12  19        x = 10:  1   2   8 [10  10] 12  19
               ^   ^                                           ^    ^   ^
           floor   ceil (LB = UB = 2)                       ceil floor  UB
           = UB-1                                           = LB = UB-1
Key facts to keep • ceil = lower bound (−1 if it equals n, i.e. everything is smaller than x)
• floor = upper bound − 1 (−1 if that's −1, i.e. everything is bigger than x)
• on duplicates: ceil gives the first copy, floor gives the last copy, which is exactly what the two questions ask for.

Part A · Floor: brute force (linear scan)

GeeksforGeeks · Floor in a Sorted Array

1The question in simple words

You get a sorted array arr and a number x. Find the largest element that is ≤ x (the floor of x) and return its index (0-based). Two extra rules:

index0123456
ex 112810111219x = 5 → 5 isn't there, the largest ≤ 5 is 2 → 1
ex 212810101219x = 11 → floor is 10, which appears twice → last one → 4
ex 312810101219x = 0 → everything is bigger → −1

2What the constraints tell us

3Intuition

Walk from left to right. As long as the value is ≤ x, it's a candidate, and since values only grow, each new candidate is better than the last one. So keep overwriting ans. The moment you see a value bigger than x, nothing later can be ≤ x, so stop.

4Building it from example 1 (x = 5)

Duplicates are handled for free: on […, 10, 10, 12] with x = 11, the second 10 overwrites the first, so we end on the last occurrence, as required.

5Approach steps

  1. ans = -1.
  2. For each index i: if arr[i] <= x → ans = i, otherwise break.
  3. Return ans.

6Code (Python)

floor, brute force: O(n)
class Solution:
    def findFloor(self, arr, x):
        ans = -1
        for i in range(len(arr)):
            if arr[i] <= x:
                ans = i          # a bigger (or equal, later) candidate
            else:
                break            # sorted: everything after is > x too
        return ans

7Code line by line

linewhat it means
ans = -1The "no floor" answer, in case even arr[0] is bigger than x.
if arr[i] <= x: ans = iThis value fits. It's at least as big as the previous candidate, so replace it.
else: breakFirst value above x. The rest are bigger still.

8Dry run (x = 5)

i012
arr[i]128
≤ 5?yesyesno → break
ans011

9Complexity & remember

Time O(n) in the worst case (x bigger than everything). Space O(1).

RememberLinear floor = "keep the last index whose value is ≤ x". It works, but it ignores that the array is sorted.

Part B · Floor: binary search

1The question

Same as Part A: index of the largest value ≤ x (last copy if repeated), or −1. Now in O(log n).

2Constraints

n ≤ 10⁶ → about 20 binary-search steps. The array is never empty, so right = n - 1 is a valid index.

3Intuition

When mid's value is too big (> x), it's useless, and so is everything to its right. When mid's value is ≤ x, it's a possible floor. Save it, but a bigger value that is still ≤ x might exist further right, so keep looking there. It's the same "save, then keep going" idea from First & Last Position.

4Building the conditions from examples

Example 1: arr = [1, 2, 8, 10, 11, 12, 19], x = 5. left = 0, right = 6 → mid = 0 + (6 − 0) // 2 = 3.

index0123456
value12810111219mid = 3 → 10

Case 1: arr[mid] > x → go left, right = mid - 1

10 is bigger than 5. A floor must be ≤ 5, so 10 can't be it, and neither can anything to its right (all ≥ 10). Search the left part. Why mid - 1 and not mid? Because we just saw that mid is too big. It can never be the answer, so there's no reason to keep it.

value12810111219right = 2
Case 1if arr[mid] > x: right = mid - 1

Case 2: arr[mid] < x → save it, go right

New mid = 0 + (2 − 0) // 2 = 1 → value 2. 2 < 5, so 2 could be the floor. But maybe there's something bigger than 2 that's still ≤ 5 on its right. Since the array is sorted, bigger values are only on the right. So:

value12810111219ans = 1, left = 2

We start with ans = -1: if no value ≤ x is ever seen, −1 is returned, as the question wants.

Case 3: arr[mid] == x → what then? (her thought experiment)

The teacher asks: what if x were 10 instead of 5 (on example 2's array [1, 2, 8, 10, 10, 12, 19])? The first mid (index 3) is 10, an exact match. Can we return 3 right away? No. The question says that for repeats we must return the last occurrence, and there's another 10 at index 4. Just like in First & Last Position, a later copy can only be on the right. So we save mid and go right.

Then she notices that this is exactly what we already do for "less than": save, then left = mid + 1. So the two cases merge into one condition with <=:

Cases 2 + 3 mergedif arr[mid] <= x: ans = mid; left = mid + 1
else: right = mid - 1
Doubt: why does merging automatically give the last occurrence on duplicates?
→ Every time mid lands on a copy, we save it and move right. The search only ends when nothing to the right is ≤ x, so the last index we saved is the rightmost one that is ≤ x. That's both the biggest value and the last copy of it.
Doubt: when do we get −1?
→ Her example: x = 0. Every value is ≥ 1 > 0, so every mid falls in Case 1, right keeps moving left until it passes −1, and ans is never touched → −1.

5Approach steps

  1. left = 0, right = len(arr) - 1, ans = -1.
  2. While left <= right: mid = left + (right - left) // 2.
  3. If arr[mid] <= x: ans = mid, left = mid + 1 (look for a bigger floor on the right).
  4. Else: right = mid - 1 (too big, look left).
  5. Return ans.

6Code (Python)

floor: binary search
class Solution:
    def findFloor(self, arr, x):
        left, right = 0, len(arr) - 1      # last INDEX, not len(arr)
        ans = -1
        while left <= right:
            mid = left + (right - left) // 2
            if arr[mid] <= x:
                ans = mid                  # a possible floor: save it
                left = mid + 1             # try for a bigger one on the right
            else:
                right = mid - 1            # too big: go left
        return ans
A slip she fixed on screenShe first wrote right = arr.length. That points one past the end, and arr[mid] could then read outside the array. She corrected it to length − 1, the last valid index. With while left <= right and mid - 1 / mid + 1 moves, right must start at n - 1.

7Code line by line

linewhat it means
left, right = 0, len(arr) - 1Search the whole array: first index to last index.
ans = -1Returned as-is if nothing is ≤ x.
while left <= right:Until the pointers cross, including the single-cell case.
mid = left + (right - left) // 2Overflow-safe middle (a habit in Python).
if arr[mid] <= x: ans = mid left = mid + 1mid fits under x. Remember it, then search right for something bigger that still fits (or a later copy of the same value).
else: right = mid - 1mid is above x. Drop it and everything to its right.
return ansThe last saved index: the floor's last occurrence, or −1.

8Dry runs (hand tables)

Example 1: [1, 2, 8, 10, 11, 12, 19], x = 5

stepleftrightmidarr[mid]decisionansthrown away
10631010 > 5 → right = 2−13–6
202122 ≤ 5 → save, left = 210–1
322288 > 5 → right = 112
end21crossed → return 1 (value 2) ✓
step 112810111219too big
step 212810111219fits → ans = 1
step 312810111219too big → done

Example 2 (duplicates): [1, 2, 8, 10, 10, 12, 19], x = 11

stepleftrightmidarr[mid]decisionans
10631010 ≤ 11 → save, left = 43
24651212 > 11 → right = 43
34441010 ≤ 11 → save, left = 54
end54crossed → 4, the last 10 ✓

Her x = 10 thought experiment on the same array follows the exact same steps (10 ≤ 10 at index 3, 12 > 10, 10 ≤ 10 at index 4) → 4. And with x = 0: mids 3 → 1 → 0 are all too big, and ans stays −1.

9Complexity & remember

Remember: floorarr[mid] <= x → save, go right. Otherwise go left. Start ans = -1, right = n - 1.

Part C · Ceiling: binary search

GeeksforGeeks · Ceil in a Sorted Array

1The question in simple words

Same input. Now find the smallest element that is ≥ x (the ceiling of x) and return its index. If none exists, return −1. If the ceiling value appears several times, return the first occurrence (floor asked for the last one).

index01234567
ex 112810111219·x = 5 → smallest ≥ 5 is 8 → 2
ex 212810111219·x = 20 → nothing ≥ 20 → −1
ex 3112810111219x = 0 → ceiling is 1, which appears twice → first → 0

2Constraints

Same as floor: up to 10⁶ elements, values 1..10⁶. A linear scan would pass (stop at the first value ≥ x), but the sorted array calls for binary search.

3Intuition: floor, flipped

For the ceiling, a value ≥ x is a possible answer, but a smaller one that's still ≥ x might be on the left. So: save it, go left. A value < x is useless, together with everything left of it, so go right. Every arrow from the floor version is reversed.

4Building the conditions from example 1 (x = 5)

Ceiling conditionsif arr[mid] >= x: ans = mid; right = mid - 1
else: left = mid + 1

The equal case is folded in again (that's the = in >=). On an exact match, we save it and keep going left, so with duplicates we end on the first copy, which is what the question asks for. Example 3 shows it: with x = 0 we save 3 (value 8), then 1 (value 1), then 0 (value 1) → answer 0.

Doubt: the teacher literally copies the floor code and edits it. What exactly changes?
→ Only the if line and which pointer moves in each branch:
• floor: <= x → save, left = mid + 1 · else right = mid - 1
• ceil: >= x → save, right = mid - 1 · else left = mid + 1
left/right setup, ans = -1, the loop and the return all stay the same.

5Approach steps

  1. left = 0, right = len(arr) - 1, ans = -1.
  2. While left <= right: compute mid.
  3. If arr[mid] >= x: ans = mid, right = mid - 1 (look for a smaller ceiling on the left).
  4. Else: left = mid + 1.
  5. Return ans.

6Code (Python)

ceiling: binary search
class Solution:
    def findCeil(self, arr, x):
        left, right = 0, len(arr) - 1
        ans = -1
        while left <= right:
            mid = left + (right - left) // 2
            if arr[mid] >= x:
                ans = mid                  # a possible ceiling: save it
                right = mid - 1            # try for a smaller one on the left
            else:
                left = mid + 1             # too small: go right
        return ans

7Code line by line

linewhat it means
if arr[mid] >= x: ans = mid right = mid - 1mid is at or above x, so it's a valid ceiling. Remember it, then search left for a smaller valid one (or an earlier copy).
else: left = mid + 1mid is below x, so it's not a ceiling. Drop it and everything to its left.
everything elseSame as floor.

8Dry run (hand table)

Example 1: [1, 2, 8, 10, 11, 12, 19], x = 5

stepleftrightmidarr[mid]decisionansthrown away
10631010 ≥ 5 → save, right = 233–6
202122 < 5 → left = 230–1
322288 ≥ 5 → save, right = 122
end21crossed → 2 (value 8) ✓
step 112810111219fits → ans = 3, go left
step 212810111219too small → go right
step 312810111219fits → ans = 2 → done

Example 3 (duplicates): [1, 1, 2, 8, 10, 11, 12, 19], x = 0

stepleftrightmidarr[mid]decisionans
10738≥ 0 → save, right = 23
20211≥ 0 → save, right = 01
30001≥ 0 → save, right = −10
end0−1crossed → 0, the first 1 ✓

With x = 20, every mid is too small, left runs past the end, and ans stays −1.

9Complexity & remember

O(log n) time, O(1) space.

Remember: ceilingarr[mid] >= x → save, go left. Otherwise go right. It's floor with every arrow flipped.

Part D · Floor & ceiling as upper / lower bound

Not shown in the video. This is added to connect these problems to the pattern's name.

1The connection

Look at what each search really finds:

index0123456
value12810101219
≤ 10 ?yesyesyesyesyesnonofloor = last yes = 4
≥ 10 ?nononoyesyesyesyesceil = first yes = 3
bounds↑LB↑UBceil = LB, floor = UB − 1

Floor and ceiling meet on the same index exactly when x is in the array once. When x is missing, the floor is right before the ceiling (floor = ceil − 1).

2Code (Python)

floor and ceil through the two bounds
def lower_bound(arr, x):          # first index with arr[i] >= x  (n if none)
    left, right, ans = 0, len(arr) - 1, len(arr)
    while left <= right:
        mid = left + (right - left) // 2
        if arr[mid] >= x:
            ans, right = mid, mid - 1
        else:
            left = mid + 1
    return ans

def upper_bound(arr, x):          # first index with arr[i] > x   (n if none)
    left, right, ans = 0, len(arr) - 1, len(arr)
    while left <= right:
        mid = left + (right - left) // 2
        if arr[mid] > x:
            ans, right = mid, mid - 1
        else:
            left = mid + 1
    return ans

def find_ceil(arr, x):
    lb = lower_bound(arr, x)
    return lb if lb < len(arr) else -1     # n means "nothing is >= x"

def find_floor(arr, x):
    return upper_bound(arr, x) - 1           # 0 - 1 = -1 means "nothing is <= x"

Notice that lower_bound here is the ceiling code with ans starting at n instead of −1, and in Python bisect.bisect_left / bisect.bisect_right are these same two bounds.

3Complexity

One binary search each: O(log n) time, O(1) space.


Part E · Revision page

floorceiling
meaninglargest value ≤ xsmallest value ≥ x
on duplicates returnlast occurrencefirst occurrence
"keep" conditionarr[mid] <= xarr[mid] >= x
thenans = mid; left = mid + 1 (go right)ans = mid; right = mid - 1 (go left)
elseright = mid - 1left = mid + 1
not foundx smaller than everything → −1x bigger than everything → −1
as a boundupper bound − 1lower bound
[1,2,8,10,11,12,19], x=51 (value 2)2 (value 8)
time / spaceO(log n) / O(1) (the linear scan is O(n))
If you remember only 5 lines 1. Floor = biggest ≤ x; ceiling = smallest ≥ x. Return the index, −1 if none.
2. If mid could be the answer → save it in ans and keep searching toward a better one.
3. Floor: ≤ x → save, go right. Ceiling: ≥ x → save, go left.
4. Folding "equal" into the save branch gives last copy (floor) / first copy (ceiling) for free.
5. right = n - 1, while left <= right, ans = -1.
Mistakes to avoid ✗ right = len(arr) with a <= loop (reads past the end; the teacher's own slip)
✗ returning immediately when arr[mid] == x (misses the last/first copy rule)
✗ returning the value instead of the index
✗ moving the wrong pointer when you copy floor into ceiling
✗ right = mid / left = mid with a <= loop (can loop forever)
test it yourself (paste under BOTH Part B and Part C solutions, merged into one class)
s = Solution()
print(s.findFloor([1, 2, 8, 10, 11, 12, 19], 5))      # 1
print(s.findFloor([1, 2, 8, 10, 10, 12, 19], 11))     # 4
print(s.findFloor([1, 2, 8, 10, 10, 12, 19], 0))      # -1
print(s.findCeil([1, 2, 8, 10, 11, 12, 19], 5))       # 2
print(s.findCeil([1, 2, 8, 10, 11, 12, 19], 20))      # -1
print(s.findCeil([1, 1, 2, 8, 10, 11, 12, 19], 0))    # 0

Based on this video: Floor in a Sorted Array & Ceil in a Sorted Array