San Cao
Backend Engineer • Infrastructure
Engineering NotesAlgorithmsC++

Binary Search: O(log n) or O(∞)?

Binary search promises O(log n). One wrong decision gives you O(∞). Here is the rule that separates them.

Binary Search: O(log n) or O(∞)?

Jon Bentley asked professional programmers to implement binary search. Over 90% wrote subtly broken code. Not because the algorithm is hard. Because they never answered one question before touching the keyboard: is rhs pointing at a valid element, or one past it?

That single omission is the root of every off-by-one error, wrong return value, and infinite loop I have ever seen in binary search.


The Interval Is the Contract

Two styles exist. Pick one. Never switch mid-implementation.

[lhs, rhs] — closed. Both ends are candidates.

 [ 10   20   30   40   50   60   70 ]
   ^              ^              ^
  lhs            mid            rhs
                                 └── valid element. will be visited.
 lhs = mid + 1      rhs = mid - 1      while (lhs <= rhs)

[lhs, rhs) — half-open. rhs is a fence post.

 [ 10   20   30   40   50   60   70 ]   ·
   ^              ^                     ^
  lhs            mid                   rhs
                                        └── never visited. one past the end.
 lhs = mid + 1      rhs = mid           while (lhs < rhs)

Mix them and you get silent wrong answers. The compiler will not save you.


The Infinite Loop

Finding the last valid element requires retaining mid as a candidate: lhs = mid.

Here is what happens when the space shrinks to two elements:

 Space of size 2:
 [ 40   50 ]
   ^    ^
  lhs  rhs

 mid = lhs + (rhs - lhs) / 2
     = 0   + (  1 -   0) / 2
     = 0   ──► resolves to lhs

 Predicate says valid → lhs = mid = 0
                                    └── no-op. pointers do not move. forever.

The predicate is correct. Floor division betrayed you.

The rule: lhs = mid always requires a ceiling midpoint. This applies to the half-open [lhs, rhs) form — the only form where lhs = mid is a valid update.

 rhs = mid  →  mid = lhs + (rhs - lhs) / 2     ← floor   (safe)
 lhs = mid  →  mid = rhs - (rhs - lhs) / 2     ← ceiling (safe)

With ceiling, the size-2 case: mid = 1 - (1-0)/2 = 1 = rhs. Predicate valid → lhs = 1. Check 1 < 1 → false. Loop exits.


The Overflow Nobody Noticed for 20 Years

Every textbook wrote mid = (lhs + rhs) / 2. In 2006, Joshua Bloch found it had been broken in Java’s standard library since 1997. On arrays near 1 billion elements, lhs + rhs silently overflows a 32-bit integer. mid goes negative. The program crashes.

 Broken:  (lhs + rhs) / 2          ← overflows at ~2³¹ elements
 Safe:    lhs + (rhs - lhs) / 2    ← distance from lhs, always fits

Never use (lhs + rhs) >> 1 in production without an explicit unsigned cast — same overflow, different syntax.


The Decision Table

GoalIntervalLooplhs updaterhs updatemid
Exact match[lhs, rhs]<= rhsmid + 1mid - 1floor
First occurrence[lhs, rhs)< rhsmid + 1midfloor
Last occurrence[lhs, rhs)< rhsmidmidceiling

Once you commit to the interval, every column follows. mid is the only choice that can still surprise you.


Jon Bentley, Programming Pearls (1986). Joshua Bloch, “Extra, Extra — Read All About It: Nearly All Binary Searches and Mergesorts are Broken”, Google Research Blog (2006). CP-Algorithms: Binary Search.