Skip to content

Chapter 7 · Search and sort

Binary search is not one template. Decide what the final position means before choosing boundaries.

Three binary-search families

Family Search interval Result
exact match inclusive [lo, hi] matching index or -1
first true half-open [lo, hi) first position satisfying predicate
answer space known bounded candidates smallest feasible value
flowchart TD
  accTitle: Choosing one of the three binary-search contracts
  accDescr: Use exact-match search when looking for a target stored in sorted data, first-true search when locating a monotone boundary, and answer-space search when candidates are ordered and a monotone feasibility test can identify the smallest feasible answer.
  A{"What does the returned<br/>position mean?"}
  A -->|"index containing target"| B["Exact match"]
  B --> C["Inclusive [lo, hi]<br/>return index or -1"]
  A -->|"first position where<br/>a property becomes true"| D["Boundary / first true"]
  D --> E["Half-open [lo, hi)<br/>return lo, possibly n"]
  A -->|"smallest numeric answer<br/>that can work"| F{"Is feasibility<br/>monotone?"}
  F -->|yes| G["Binary search on answer"]
  G --> H["Known lower bound +<br/>known feasible upper bound"]
  F -->|no| I["Binary search is invalid:<br/>change the model"]

All three repeatedly discard half of an ordered candidate set, but they cannot share boundaries blindly because their return contracts differ.

Exact match

Invariant: if the target exists, it remains inside the inclusive search interval.

def binary_search(values: Sequence[T], target: T) -> int:
    """Return any index containing ``target``, or ``-1`` when absent."""
    # Invariant: a present target remains inside inclusive [low, high].
    low, high = 0, len(values) - 1
    while low <= high:
        middle = low + (high - low) // 2
        if values[middle] == target:
            return middle
        if values[middle] < target:
            # middle and everything left of it are too small.
            low = middle + 1
        else:
            # middle and everything right of it are too large.
            high = middle - 1
    return -1
inline int binary_search_index(
    const std::vector<int>& values, int target) {
  // Invariant: a present target remains inside inclusive [low, high].
  int low = 0;
  int high = static_cast<int>(values.size()) - 1;
  while (low <= high) {
    const int middle = low + (high - low) / 2;
    if (values[middle] == target) {
      return middle;
    }
    if (values[middle] < target) {
      // middle and everything left of it are too small.
      low = middle + 1;
    } else {
      // middle and everything right of it are too large.
      high = middle - 1;
    }
  }
  return -1;
}
inline int binary_search_index(
    const std::vector<int>& values, int target) {
  // Invariant: a present target remains inside inclusive [low, high].
  int low = 0;
  int high = static_cast<int>(values.size()) - 1;
  while (low <= high) {
    const int middle = low + (high - low) / 2;
    if (values[middle] == target) {
      return middle;
    }
    if (values[middle] < target) {
      // middle and everything left of it are too small.
      low = middle + 1;
    } else {
      // middle and everything right of it are too large.
      high = middle - 1;
    }
  }
  return -1;
}

First true

The predicate must be monotone:

false false false true true true
                  ^
              first true

Invariant: every position before lo is false, and the first true position remains in [lo, hi). Returning n is meaningful: no position is true.

def first_true(size: int, predicate: Callable[[int], bool]) -> int:
    """Return the first true index in ``[0, size)``, or ``size``."""
    if size < 0:
        raise ValueError("size must be nonnegative")
    # [low, high) always contains the first true position or sentinel size.
    low, high = 0, size
    while low < high:
        middle = low + (high - low) // 2
        if predicate(middle):
            # middle may be the answer, so keep it.
            high = middle
        else:
            # middle is false; discard the false prefix through middle.
            low = middle + 1
    return low
template <class Predicate>
int first_true(int size, Predicate predicate) {
  if (size < 0) {
    throw std::invalid_argument("size must be nonnegative");
  }
  // [low, high) contains the first true position or sentinel size.
  int low = 0;
  int high = size;
  while (low < high) {
    const int middle = low + (high - low) / 2;
    if (predicate(middle)) {
      // middle may be the answer, so keep it.
      high = middle;
    } else {
      // middle is false; discard the false prefix through middle.
      low = middle + 1;
    }
  }
  return low;
}
template <class Predicate>
int first_true(int size, Predicate predicate) {
  if (size < 0) {
    throw std::invalid_argument("size must be nonnegative");
  }
  // [low, high) contains the first true position or sentinel size.
  int low = 0;
  int high = size;
  while (low < high) {
    const int middle = low + (high - low) / 2;
    if (predicate(middle)) {
      // middle may be the answer, so keep it.
      high = middle;
    } else {
      // middle is false; discard the false prefix through middle.
      low = middle + 1;
    }
  }
  return low;
}

Binary search on the answer

Use this when:

  1. the answer is numeric or ordered;
  2. you can test feasibility for a candidate;
  3. feasibility changes monotonically;
  4. you know an inclusive range containing at least one feasible candidate.

Write and test the feasibility function separately. Its complexity multiplies the logarithmic search count.

Sorting as a structural transformation

Sorting is useful when order makes candidate elimination provable: merge intervals, two pointers, sweep lines, greedy scheduling, and adjacency-based duplicate skipping. Record whether sorting mutates input and whether original indices must be retained.

Practice ladder · write the search contract

Rung Problem Thinking prompt
1 Binary Search What exactly means “not found”?
2 Find First and Last Position Which two monotone boundaries define the answer?
3 Search in Rotated Sorted Array Which half is provably sorted this iteration?
4 Koko Eating Bananas Why does feasibility stay true above the boundary?

State the interval style and final return meaning before calculating a midpoint.