02 — Binary Search & Monotonic Predicates
At a glance
After completing this lesson you can:
- Implement off-by-one bug-free binary search using lower and upper bounds (
bisect_leftvsbisect_right). - Apply "binary search on answer" to monotonic feasibility predicates across search spaces.
- Quickly search multi-gigabyte timestamp-ordered log files in $O(\log n)$ disk page reads.
Time: ~50 minutes · Prerequisites: 01 — Two Pointers & Sliding Windows
Reading Companion
- Book: Grokking Algorithms (2nd Edition) — Aditya Y. Bhargava
- Chapter 1: Binary Search (The search problem, Logarithmic time).
Why this matters in production
Database B-Tree indexes, Kafka partition offsets, and timestamped log indexes all rely on binary search. When querying a time range in a 50 GB log file, scanning from line 1 takes minutes ($O(n)$). Because log entries are strictly monotonic in time, binary searching file byte offsets locates the exact starting minute in ~25 seeks ($O(\log n)$), returning metrics instantly.
Deep dive
1. The Monotonicity Condition
Binary search works on ANY problem with a monotonic predicate $P(x)$:
x: 1 2 3 4 5 6 7 8
P(x): F F F F T T T T
^
First True (Transition Point)
2. Standard Binary Search Boundaries (Template)
To avoid infinite loops and boundary bugs:
def binary_search_first_true(low: int, high: int, predicate) -> int:
while low < high:
mid = (low + high) // 2
if predicate(mid):
high = mid # Answer could be mid or to the left
else:
low = mid + 1 # Mid is definitely False, move right
return low
Hands on
python3 phase1-cs-and-architecture/02-algorithmic-patterns/02-binary-search-and-intervals/examples/01_demo.py
python3 phase1-cs-and-architecture/02-algorithmic-patterns/02-binary-search-and-intervals/examples/02_trap.py
AI Trap & Code Review
# BAD AI-GENERATED CODE — INTEGER OVERFLOW / INFINITE LOOP ON MID
def binary_search_buggy(arr, target):
low = 0
high = len(arr) - 1
while low <= high:
mid = (low + high) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
low = mid # Bug: Infinite loop when high - low == 1!
else:
high = mid
return -1
Exercises
- Rung 1 (
find_first_ge): Implement lower bound (bisect_left) returning the first index wherearr[i] >= targetin sorted arrayarr. - Rung 2 (
search_timestamp_range— Fix the AI Bug): Fix an off-by-one error in a log range filter returning start and end indices for[start_ts, end_ts]. - Rung 3 (
binary_search_square_root): Compute integer square root of $n \ge 0$ in $O(\log n)$ without using floating point ormath.sqrt. - Rung 4 (
capacity_to_ship_packages): Given package weights and $D$ delivery days, find the minimum conveyor capacity needed to deliver within $D$ days using binary search on answer. - Rung 5 (
find_peak_in_mountain): Find peak element index in a strictly increasing then strictly decreasing mountain array in $O(\log n)$ time.
Wrapping up
Can you explain?
- Why must
loworhighstrictly advance in binary search to prevent infinite loops? - What is the difference between
bisect_leftandbisect_righton duplicate values? - How does "binary search on answer" work when the underlying dataset is not sorted?
Further Reading & References
- CPython Source: listobject.c (Dynamic Array Implementation)
- Raymond Hettinger: Modern Dictionaries by Key-Sharing
- Alex Xu: System Design Interview — Rate Limiter Design
- Tim Roughgarden: Algorithms Illuminated Series
Show reference solution notes
Solution & Engineering Notes — 02 Binary Search
bisect_leftvsbisect_right:bisect_leftfinds the first index wherex >= target.bisect_rightfinds the first index wherex > target. To get the inclusive range of duplicate keys, use[bisect_left(start), bisect_right(end) - 1].- Binary search on feasibility: If a decision function
can_ship(cap)is monotonically non-decreasing (if it works for $C$, it always works for $C + 1$), binary search identifies the minimum viable capacity in $O(\log(\sum W))$ evaluations.
Self-check
Run python3 -m unittest phase1-cs-and-architecture/02-algorithmic-patterns/02-binary-search-and-intervals/test_exercises.py -v until green, then record it here. Saved to progress/rungs.json; commit it.
Completion edits PROGRESS.md; commit it with the lesson.