“Know how to solve every problem that has been solved.”“What I cannot create, I do not understand.”— Richard Feynman
Binary Search
Algorithms
⛓
What you need to know first
1 concepts, 1 layers
The requisite-knowledge inventory for this page, bottom-up: the
primitives at the base, combined upward until you reach what this page
assumes. Skim the layers you already own; start wherever the ground gets unfamiliar.
Binary search, the quintessential algorithm that opens new frontiers for searching. The funny thing about binary search is that you probably don't do it too often in real life. Dijkstra's algorithm almost reads like common sense — hey, we went down that road and it took longer, so don't go that way anymore. If I'm looking on a bookshelf I just kind of scan left to right looking for the book I need: good old linear search.
Let's do a little thought experiment. If I ask you to guess a number between 1 and 100 and you get three guesses, after each guess you get the information "higher" or "lower." Which is your best guess, and which is your worst guess? Well, if you guess 1 or 100, you've eliminated exactly 1 number by choosing that number. So unless you're trying to lose, don't choose that number. Now, 99 or 2 is a better choice because you eliminate 2 numbers — guess 99 and learn "lower," and you know the answer isn't 99 or anything above it. Starting to see a pattern? The best number to guess starting from the beginning is 50; after that it's halfway between, etc., etc. This is the intuition of binary search. If you've ever used a bisection method in numerical analysis it's strikingly similar. It also kind of reminds me of Zeno's paradox, except instead of halving a physical distance you're halving a range of numbers to converge on the target.
Algorithm
Let's actually play it out. Say the number is 81 (you don't know that yet — that's the whole point). You start with the range 1-100. Guess 50, the middle. "Higher." The range collapses to 51-100, exactly half what it was. Guess 75, the middle of what's left. "Higher" again. Down to 76-100, twenty-five numbers. Guess 88. "Lower." Down to 76-87, twelve numbers. Guess 81. Got it, in four guesses. On a bad day you'd take about seven — because log2(100)≈6.6, and "halve a hundred until you get to one" takes about that many rounds. That last sentence is the entire reason binary search has the runtime it does.
Given a sorted array A and a target value x:
Compare x with the middle element
If x matches the middle element, return its index
If x is greater than the middle element, search the right half
If x is less than the middle element, search the left half
Repeat until the element is found or the search space is exhausted
Time Complexity
The "log" isn't magic — it's just what you get when you cut the size in half over and over. Start with n. After one step, n/2. After two, n/4. After three, n/8. After k steps, n/2k. You're done when that's down to one, which means 2k=n, which means k=log2(n). The exponent and the logarithm are inverses of each other, and that inverse relationship is where every logn in algorithm complexity ultimately comes from.
Implementation
def binary_search(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 + 1 else: high = mid - 1 return -1 # Return -1 if the target is not found# Example usage with a sorted arrayarr = [12, 23, 34, 45, 56, 78, 89]target = 56result = binary_search(arr, target)if result != -1: print("Element {} found at index {}.".format(target, result))else: print("Element {} not found in the array.".format(target))
Edge cases that bite people
The implementation looks simple, but a few details have caused real outages. Worth knowing before you write one in an interview.
While this is not a math textbook, let's begin by examining pathological cases. The first flavor is integer overflow on fixed width. The problem is that you have two integers that are both fine, but their sum is sum-thing else — you know, the case where the sum doesn't actually give you the sum. In Java this manifests as a negative integer. In the proud tradition of numerical analysts we will find some way to add zero to this term in order to get the midpoint formula behaving correctly. Joshua Bloch wrote the following solution (see the Google Research blog post for the full archaeological dig):
int mid = low + (high - low) / 2; // the "+ 0" that saves you
This is not really a problem in Python because there are no fixed widths, but for your own educational experience the pioneering minds at notesoncomputing.com have engineered a Python fixed-width simulator for your browser.
INT_MIN = -2147483648INT_MAX = 2147483647
Unsafe:(low + high) / 2✗ overflow
low + high= 4100000000 (true math)
wrapped to 32 bits= -194967296
/ 2 →-97483648arr[mid] would crash
Safe:low + (high - low) / 2✓ ok
high - low= 100000000
(high - low) / 2= 50000000
low + (high - low) / 2 →2050000000
True midpoint (unbounded math)2050000000
< vs <= in the loop. If you write while low <= high (the code above), you're doing exact-match binary search — return early when arr[mid] == target, return -1 if you fall out. If you write while low < high, you're doing a lower_bound variant — return low at the end, which is the insertion point where the target would go if it's missing. Both correct, just different algorithms. Mixing them up is the source of half the bugs in binary search.
The mid + 1 and mid - 1 updates are not optional. Once you've ruled out mid, you have to exclude it from the next range or the loop won't terminate. Setting low = mid instead of low = mid + 1 on the "greater than" branch is the classic infinite loop, and it only shows up when low and high become adjacent and mid == low. Pleasant.
What do you return when the target isn't there? Three conventions exist in the wild. Return -1 (simplest). Return the insertion point (more useful — tells the caller where to put the element). Or return -(insertion_point + 1) (Java's choice — distinguishes found from not-found while still encoding the position). Pick whichever the caller needs and document it. The version above uses the first.
Duplicates. If the array has multiple matching elements, standard binary search returns some matching index — possibly any of them, depending on the access pattern. If you need the leftmost or rightmost match specifically, you need a different variant: bisect_left and bisect_right in Python, lower_bound and upper_bound in C++.
Try First
Each prompt asks a checkable question about the working code or math
above — predict an output, derive a sign, state an invariant, find a
bug. Commit to an answer before clicking "reveal." That commitment is
the whole point: if your answer matched, you understand the piece you
were looking at; if it didn't, that's the part worth re-reading.
trace
Walk through binary_search([12, 23, 34, 45, 56, 78, 89], 56)
by hand. List the sequence of mid values the loop visits and
what each comparison says.
answer
Three iterations:
low=0, high=6, mid=3 → arr[3]=45 < 56 → set low=4
low=4, high=6, mid=5 → arr[5]=78 > 56 → set high=4
Three guesses for seven elements — roughly log₂(7) ≈ 2.8,
which checks out.
predict
Before running it, predict the return value of
binary_search([1, 3, 5, 7], 4). The target isn't in the array
— but where does the loop "land" before giving up?
hint
The function returns -1 when it falls out of the loop.
The interesting question is what low and high
are at that moment.
answer
Returns -1.
low=0, high=3, mid=1 → arr[1]=3 < 4 → low=2
low=2, high=3, mid=2 → arr[2]=5 > 4 → high=1
Now low=2 > high=1, loop exits.
Note that low ends up at 2 — the insertion
index where 4 would go to keep the array sorted. That's
not an accident; it's the bisect_left result hiding
inside the exact-match search.
invariant
State the invariant that low and high maintain
at the top of every loop iteration. Phrase it in terms of where the
target can possibly still be.
answer
At the top of each iteration, if the target exists in the array at all,
it must be at some index i with low ≤ i ≤ high.
Equivalently: every element with index < low has been
ruled out as strictly less than the target, and every element with
index > high has been ruled out as strictly greater.
The loop's only job is to shrink that window while preserving this
property — which is exactly what the mid ± 1 updates do.
find the bug
I changed line 4 from while low <= high to
while low < high. The function still compiles and still
handles most inputs. Find an array and a target where the buggy version
returns -1 for a target that is in the array.
hint
Think about when low and high become equal.
With the original condition the loop runs one more time; with the bug
it exits immediately.
answer
Anything where the target ends up at the position where
low == high. The minimal case is a single-element array:
binary_search([5], 5). Original returns 0;
buggy returns -1 because 0 < 0 is false
and the loop never executes. Other small triggers:
binary_search([1, 3], 3) and
binary_search([1, 3], 1) — each ends with
low == high at the answer index.
what if
Remove the + 1 from low = mid + 1 on line 9
(now low = mid). The page above already calls this out as
"the classic infinite loop." Find the smallest concrete input that
actually triggers it.
answer
Any input where the loop reaches a state with
high == low + 1 and arr[mid] < target.
Minimal example: binary_search([1, 5], 5).
low=0, high=1, mid=0 → arr[0]=1 < 5 → set low=mid=0
State is unchanged. Repeat forever.
The + 1 isn't decoration; it's the mechanism that makes
the search space strictly shrink each iteration.
why does this work
Without re-deriving the runtime, answer this: why is the worst-case
iteration count ⌈log₂(n)⌉ rather than n/2?
Both involve "halving," after all. What does the algorithm halve, and
what does it keep?
answer
Each iteration halves the size of the remaining search space,
not the array. After one step, n candidates become at most
n/2. After two, n/4. After k
steps, n / 2^k. The loop stops when that drops below
1, which happens at k = log₂(n).
n/2 is what you'd get if you halved once and
then did linear search on the remaining half — a different (and
worse) algorithm. Binary search's entire advantage is that the
halving compounds, and compounding halving gives a logarithm. This
is the same reason that ten coin flips have 2^10 = 1024
outcomes, not 20.
Exercises
A full exercise set is available for this topic, structured as one worked example + 7 practice problems (across 7 surface contexts) + 2 pattern-resistant check problems.