Binary search

Binary search

Why binary search works, the invariant you must say out loud, lower_bound templates, and search-on-answer with a full shipping-capacity walkthrough.

Why it works

Binary search
Each mid test throws away half the range.

If a range is sorted (or a yes/no answer is monotonic), testing the middle tells you which half can be discarded. The skill in interviews isn’t the while-loop — it’s stating an invariant so you don’t off-by-one yourself.

Analogy: dictionary flip

Binary search dictionary
Compare the middle page; throw away half.

Binary search is how you find a word in a paper dictionary: open the middle, decide left or right half, repeat. The skill isn’t the loop — it’s the sentence “everything left of lo is too small; everything at/right of hi is big enough.”

Template + tiny trace

def lower_bound(a, x):
    # first index i with a[i] >= x
    lo, hi = 0, len(a)
    while lo < hi:
        mid = (lo + hi) // 2
        if a[mid] < x:
            lo = mid + 1   # mid is too small
        else:
            hi = mid       # mid might be answer
    return lo

# a = [1,3,3,7], x = 3 → answers index 1
# start lo=0 hi=4 mid=2 (3) → hi=2
# mid=1 (3) → hi=1
# lo==hi==1 stop
int lowerBound(int[] a, int x) {
    // first index i with a[i] >= x
    int lo = 0, hi = a.length;
    while (lo < hi) {
        int mid = lo + (hi - lo) / 2;
        if (a[mid] < x) {
            lo = mid + 1;   // mid is too small
        } else {
            hi = mid;       // mid might be answer
        }
    }
    return lo;
}

// a = [1,3,3,7], x = 3 → answers index 1
// start lo=0 hi=4 mid=2 (3) → hi=2
// mid=1 (3) → hi=1
// lo==hi==1 stop

Dry-run one example on the board before coding. Candidates who skip this often flip lo/hi updates under pressure.

Search on answer — full example

Problem shape: minimize capacity C to ship all packages in D days. Feasibility is monotonic: if C works, C+1 works. So binary search C.

def min_capacity(weights, days):
    def ok(cap):
        need = 1
        cur = 0
        for w in weights:
            if cur + w > cap:
                need += 1
                cur = 0
            cur += w
        return need <= days

    lo, hi = max(weights), sum(weights)
    while lo < hi:
        mid = (lo + hi) // 2
        if ok(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo

# weights=[1,2,3,4,5,6,7,8,9,10], days=5
# lo starts 10, hi 55 — search mid capacities until minimal ok
int minCapacity(int[] weights, int days) {
    int lo = 0, hi = 0;
    for (int w : weights) { lo = Math.max(lo, w); hi += w; }
    while (lo < hi) {
        int mid = lo + (hi - lo) / 2;
        if (ok(weights, days, mid)) hi = mid;
        else lo = mid + 1;
    }
    return lo;
}
boolean ok(int[] weights, int days, int cap) {
    int need = 1, cur = 0;
    for (int w : weights) {
        if (cur + w > cap) { need++; cur = 0; }
        cur += w;
    }
    return need <= days;
}

// weights=[1,2,3,4,5,6,7,8,9,10], days=5
// lo starts 10, hi 55 — search mid capacities until minimal ok

What a good explanation sounds like

You: ‘The answer capacity is monotonic — if 30 works, 31 works — so I’ll binary search capacity between max(weight) and sum(weights). At mid I’ll simulate days needed; if too many days, mid is too small and I raise lo.’

That paragraph is the interview. The code is just the receipt.

When to reach for this pattern

Two flavors only: (1) find a position in a sorted sequence, (2) binary search the answer value because feasibility is monotonic. Interviews punish fuzzy lo/hi updates — pick one invariant and stick to it.

Core template + worked trace

Lower bound — first index with a[i] >= x. Half-open [lo, hi).

def lower_bound(a: list[int], x: int) -> int:
    # Invariant: all indices < lo are < x; all indices >= hi are >= x (or past end)
    lo, hi = 0, len(a)
    while lo < hi:
        mid = (lo + hi) // 2
        if a[mid] < x:
            lo = mid + 1   # mid too small
        else:
            hi = mid       # mid might be first good
    return lo

# a=[1,3,3,7], x=3
# lo,hi=0,4 mid=2 (3) → hi=2
# mid=1 (3) → hi=1
# lo==hi==1 → first 3
int lowerBound(int[] a, int x) {
    // Invariant: all indices < lo are < x; all indices >= hi are >= x (or past end)
    int lo = 0, hi = a.length;
    while (lo < hi) {
        int mid = lo + (hi - lo) / 2;
        if (a[mid] < x) {
            lo = mid + 1;   // mid too small
        } else {
            hi = mid;       // mid might be first good
        }
    }
    return lo;
}

// a=[1,3,3,7], x=3
// lo,hi=0,4 mid=2 (3) → hi=2
// mid=1 (3) → hi=1
// lo==hi==1 → first 3

Search-on-answer skeleton (shipping capacity style):

def min_feasible(lo: int, hi: int, ok) -> int:
    # ok(mid) True ⇒ mid works; assume monotonic: ok(m) ⇒ ok(m+1)
    while lo < hi:
        mid = (lo + hi) // 2
        if ok(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo
int minFeasible(int lo, int hi, java.util.function.IntPredicate ok) {
    // ok(mid) true ⇒ mid works; assume monotonic: ok(m) ⇒ ok(m+1)
    while (lo < hi) {
        int mid = lo + (hi - lo) / 2;
        if (ok.test(mid)) hi = mid;
        else lo = mid + 1;
    }
    return lo;
}

Edge cases & common bugs

Complexity — say it aloud

Interview talk track

You: ‘I’ll maintain the invariant that everything left of lo is too small and everything at or right of hi is big enough. I shrink until lo == hi — the first feasible answer. For capacity, I’ll prove monotonicity: if C works, C+1 works.’

Practice set

  • Binary Search / Lower Bound
  • Search Insert Position
  • Find First and Last Position in Sorted Array
  • Search in Rotated Sorted Array
  • Koko Eating Bananas
  • Capacity To Ship Packages Within D Days
  • Split Array Largest Sum
  • Median of Two Sorted Arrays (stretch)

Harder follow-up

Harder variant: Split Array Largest Sum — minimize the largest subarray sum when splitting into m parts. Same search-on-answer; ok(cap) counts how many parts you need greedily.

Pattern bank: more questions + efficient solutions

Five search staples. Always name the predicate: ‘first true’, ‘last false’, or ‘minimum feasible capacity’.

Q: Search in Rotated Sorted Array — nums was sorted ascending then rotated; find target’s index or −1. Assume unique elements.

def search_rotated(nums, target):
    lo, hi = 0, len(nums) - 1
    while lo <= hi:
        mid = (lo + hi) // 2
        if nums[mid] == target:
            return mid
        if nums[lo] <= nums[mid]:          # left half sorted
            if nums[lo] <= target < nums[mid]:
                hi = mid - 1
            else:
                lo = mid + 1
        else:                               # right half sorted
            if nums[mid] < target <= nums[hi]:
                lo = mid + 1
            else:
                hi = mid - 1
    return -1
# O(log n) time, O(1) space
int searchRotated(int[] nums, int target) {
    int lo = 0, hi = nums.length - 1;
    while (lo <= hi) {
        int mid = lo + (hi - lo) / 2;
        if (nums[mid] == target) return mid;
        if (nums[lo] <= nums[mid]) { // left sorted
            if (nums[lo] <= target && target < nums[mid]) hi = mid - 1;
            else lo = mid + 1;
        } else { // right sorted
            if (nums[mid] < target && target <= nums[hi]) lo = mid + 1;
            else hi = mid - 1;
        }
    }
    return -1;
}
// O(log n) time, O(1) space

Q: Find First and Last Position of Element in Sorted Array — return [first, last] indices of target, or [−1, −1] if absent.

def search_range(nums, target):
    def lower(x):
        lo, hi = 0, len(nums)
        while lo < hi:
            mid = (lo + hi) // 2
            if nums[mid] < x:
                lo = mid + 1
            else:
                hi = mid
        return lo
    L = lower(target)
    if L == len(nums) or nums[L] != target:
        return [-1, -1]
    R = lower(target + 1) - 1
    return [L, R]
# O(log n) time, O(1) space
int[] searchRange(int[] nums, int target) {
    int L = lowerBound(nums, target);
    if (L == nums.length || nums[L] != target) return new int[]{-1, -1};
    int R = lowerBound(nums, target + 1) - 1;
    return new int[]{L, R};
}
int lowerBound(int[] a, int x) {
    int lo = 0, hi = a.length;
    while (lo < hi) {
        int mid = lo + (hi - lo) / 2;
        if (a[mid] < x) lo = mid + 1;
        else hi = mid;
    }
    return lo;
}
// O(log n) time, O(1) space

Q: Koko Eating Bananas — piles of bananas, h hours; find minimum integer speed k such that Koko finishes all piles in ≤ h hours (ceil division per pile).

import math

def min_eating_speed(piles, h):
    lo, hi = 1, max(piles)
    while lo < hi:
        mid = (lo + hi) // 2
        hours = sum(math.ceil(p / mid) for p in piles)
        if hours <= h:
            hi = mid
        else:
            lo = mid + 1
    return lo
# O(n log M) time, O(1) space
int minEatingSpeed(int[] piles, int h) {
    int lo = 1, hi = 0;
    for (int p : piles) hi = Math.max(hi, p);
    while (lo < hi) {
        int mid = lo + (hi - lo) / 2;
        long hours = 0;
        for (int p : piles) hours += (p + mid - 1L) / mid;
        if (hours <= h) hi = mid;
        else lo = mid + 1;
    }
    return lo;
}
// O(n log M) time, O(1) space

Q: Median of Two Sorted Arrays — find median of two sorted arrays in O(log(min(m, n))) by partitioning (hard; sketch is interview-OK).

def find_median_sorted_arrays(a, b):
    if len(a) > len(b):
        a, b = b, a
    m, n = len(a), len(b)
    lo, hi = 0, m
    half = (m + n + 1) // 2
    while lo <= hi:
        i = (lo + hi) // 2          # cut in a
        j = half - i                # cut in b
        a_left = a[i - 1] if i else float("-inf")
        a_right = a[i] if i < m else float("inf")
        b_left = b[j - 1] if j else float("-inf")
        b_right = b[j] if j < n else float("inf")
        if a_left <= b_right and b_left <= a_right:
            if (m + n) % 2:
                return float(max(a_left, b_left))
            return (max(a_left, b_left) + min(a_right, b_right)) / 2.0
        if a_left > b_right:
            hi = i - 1
        else:
            lo = i + 1
    raise ValueError("unreachable")
# O(log(min(m,n))) time, O(1) space
double findMedianSortedArrays(int[] a, int[] b) {
    if (a.length > b.length) return findMedianSortedArrays(b, a);
    int m = a.length, n = b.length;
    int lo = 0, hi = m, half = (m + n + 1) / 2;
    while (lo <= hi) {
        int i = (lo + hi) / 2, j = half - i;
        int aLeft = i == 0 ? Integer.MIN_VALUE : a[i - 1];
        int aRight = i == m ? Integer.MAX_VALUE : a[i];
        int bLeft = j == 0 ? Integer.MIN_VALUE : b[j - 1];
        int bRight = j == n ? Integer.MAX_VALUE : b[j];
        if (aLeft <= bRight && bLeft <= aRight) {
            if (((m + n) & 1) == 1) return Math.max(aLeft, bLeft);
            return (Math.max(aLeft, bLeft) + Math.min(aRight, bRight)) / 2.0;
        }
        if (aLeft > bRight) hi = i - 1;
        else lo = i + 1;
    }
    throw new IllegalStateException();
}
// O(log(min(m,n))) time, O(1) space

Q: Split Array Largest Sum — split nums into m non-empty contiguous subarrays; minimize the largest subarray sum.

def split_array(nums, m):
    def ok(cap):
        pieces = 1
        cur = 0
        for x in nums:
            if cur + x > cap:
                pieces += 1
                cur = x
                if pieces > m:
                    return False
            else:
                cur += x
        return True
    lo, hi = max(nums), sum(nums)
    while lo < hi:
        mid = (lo + hi) // 2
        if ok(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo
# O(n log S) time, O(1) space
int splitArray(int[] nums, int m) {
    int lo = 0, hi = 0;
    for (int x : nums) { lo = Math.max(lo, x); hi += x; }
    while (lo < hi) {
        int mid = lo + (hi - lo) / 2;
        if (canSplit(nums, m, mid)) hi = mid;
        else lo = mid + 1;
    }
    return lo;
}
boolean canSplit(int[] nums, int m, int cap) {
    int pieces = 1, cur = 0;
    for (int x : nums) {
        if (cur + x > cap) { pieces++; cur = x; if (pieces > m) return false; }
        else cur += x;
    }
    return true;
}
// O(n log S) time, O(1) space

Pattern drill: 45 minutes

← Lattice