datarekha
Coding Patterns Easy Asked at AmazonAsked at GoogleAsked at Meta

Implement binary search correctly — and explain the off-by-one traps.

The short answer

Binary search halves the search space each iteration to find a target in O(log n). The tricky part is not the idea but the boundary conditions: closed vs. half-open intervals, how to update lo/hi, and when to use lo < hi vs. lo <= hi. One clean template eliminates all the classic bugs.

How to think about it

Nobody fails this question on the idea — “halve the search space, O(log n)” is the easy part. What the interviewer is really watching is whether your boundary conditions are clean. Do you mix lo <= hi with hi = mid? Do you write lo = mid and loop forever? The whole signal is in the three lines that update lo and hi, so the move is to pick one interval convention and stay inside it.

Binary search applies any time the space is sorted or monotone and one comparison can throw away half of it. That extends well beyond arrays — answer ranges (“the smallest feasible value”), time, even function outputs. The cue to listen for is “the space is ordered, and one check cuts it in half.”

The cleanest convention is the closed interval [lo, hi], where both ends are live candidates. You loop while lo <= hi, take mid = lo + (hi - lo) // 2 (the subtraction form dodges integer overflow in Java or C++; Python ints are unbounded, but it is a good habit). If nums[mid] == target you are done; if it is too small the answer lives in [mid+1, hi], so lo = mid + 1; if it is too big, hi = mid - 1. Move at least one step every time — that is what guarantees the loop ends.

A worked example

def binary_search(nums, target):
    lo, hi = 0, len(nums) - 1
    while lo <= hi:                       # closed interval: both ends valid
        mid = lo + (hi - lo) // 2         # subtraction form avoids overflow
        if nums[mid] == target:
            return mid
        elif nums[mid] < target:
            lo = mid + 1                  # answer is to the right
        else:
            hi = mid - 1                  # answer is to the left
    return -1                             # never found

# Find the leftmost occurrence (lower bound) — a SEPARATE half-open template
def lower_bound(nums, target):
    lo, hi = 0, len(nums)                 # hi is ONE past the last index
    while lo < hi:                        # strict less-than, not <=
        mid = lo + (hi - lo) // 2
        if nums[mid] < target:
            lo = mid + 1
        else:
            hi = mid                      # keep mid as a candidate
    return lo                             # index of first element >= target

print(binary_search([1, 3, 5, 7, 9], 7))   # exact hit
print(binary_search([1, 3, 5, 7, 9], 6))   # absent
print(lower_bound([1, 2, 2, 2, 3], 2))     # first 2
print(lower_bound([1, 2, 2, 2, 3], 4))     # past the end
print(binary_search([], 1))                # empty array
3
-1
1
5
-1

Read the outputs against the two templates. The exact-match search returns index 3 for 7 and -1 for the missing 6. The lower-bound search points at the first 2 (index 1) when the value exists, and lands at 5 — one past the last index — when nothing is >= 4, which is exactly the “insertion point” semantics you want. The empty-array call returns -1 without ever entering the loop, because lo=0, hi=-1 fails lo <= hi on the first check.

Keep practising

All Coding Patterns questions

Explore further

Skip to content