DSA · Searching
Throw away half the array, every time
Binary search finds a value in a sorted list of a million items in 20 reads. You will trace the low / mid / high window by hand, write the overflow-safe mid, and add the one-line tweak that returns the first match instead of any match.
Step the window through a 7-element array →01 The idea
Halving beats scanning
You have a sorted list of a million roll numbers and you want to know whether 812345 is in it. Reading them one at a time can cost a million reads. Binary search reads the middle one instead. That single read tells you which half the answer cannot be in, and you delete that half — half a million candidates gone for the price of one comparison.
Repeat, and the candidate count goes 1,000,000 → 500,000 → 250,000 and onward. After 19 reads at most one candidate is left, and the 20th read either finds the target sitting there or empties the window for good — 20 reads, worst case, for a million values. The only thing you pay for that is the precondition: the list must already be sorted, because the whole argument rests on "everything to the left of a bigger value is also bigger".
02 Worked example
Finding the first 4 in 1 3 4 4 4 7 9
Seven values, already sorted, with the target repeated three times. The window starts as the whole array — low = 0, high = 6 — and you are hunting for 4. The first three rows are the three passes of the loop; the fourth is the exit check that finds the window empty, and the fifth is the answer. On a loop row, read the label for the current window, the rose box for the element actually read, and the note for what happened next.
Three reads, and the candidate count went 7 → 3 → 1 → 0. Notice what did the real work: the first read found a 4 immediately, but instead of returning, the trace wrote the index down and shrank the window to the left. Record, then keep going — that single decision is the whole difference between the two versions of the code below.
03 The code
Plain search, then the leftmost tweak
Both versions carry the same window low..high with both ends inclusive, compute mid the same way, and both stop when low passes high. Four lines change, and only one of them is a real idea: line 4 records the match instead of returning it. Line 1 adds the ans variable to record into, line 5 becomes an else if because line 4 no longer exits the loop, and line 7 returns ans rather than a bare −1.
Plain — returns any match
low = 0; high = n − 1while low <= high: mid = low + (high − low) / 2 # floor if a[mid] == target: return mid if a[mid] < target: low = mid + 1 else: high = mid − 1return −1 # window empty
First occurrence — returns the leftmost
low = 0; high = n − 1; ans = −1while low <= high: mid = low + (high − low) / 2 if a[mid] == target: ans = mid; high = mid − 1 else if a[mid] < target: low = mid + 1 else: high = mid − 1return ans # smallest matching index
Why low + (high − low) / 2 and not (low + high) / 2? They are the same number in mathematics, not in fixed-width integers. If low and high are both near the 32-bit limit, the sum wraps around to a negative number and mid indexes out of bounds. The subtraction form never builds a value larger than high, so it cannot wrap. This exact bug sat inside the Java standard library binary search for nine years.
Why record instead of return? Returning on the first match is what made the trace stop at index 3. Saving mid into ans and then setting high = mid − 1 says "I have a valid answer, now prove there is an earlier one". Every later match overwrites ans with a smaller index, so when the window finally empties, ans holds the leftmost one.
Why high = mid − 1 and never high = mid? Every branch has to move a boundary past mid, so the window loses at least the middle element each pass. In a two-element window mid equals low, so high = mid would leave the window unchanged and the loop would spin forever. That same guarantee is the termination proof: a window that always shrinks must reach empty.
05 Cheat sheet
Numbers worth memorising
| Case | Cost | What causes it |
|---|---|---|
| Best case, plain search | O(1) | The very first mid is the target. On 1 3 4 4 4 7 9 that happens: a[3] is 4, so plain search finishes in 1 read. |
| Average and worst case | O(log n) | The window halves each pass. At most ⌊log₂ n⌋ + 1 reads before it empties. |
| First-occurrence variant | O(log n) | Never stops early — it runs the window down to empty every time, so there is no O(1) best case. On n = 7 that is always 3 reads; on n = 1,000,000 it is 19 or 20. |
| Space, iterative | O(1) | Three integers: low, high, mid. |
| Space, recursive | O(log n) | One stack frame per halving, unless the compiler eliminates the tail call. |
| Sorting first, if the input is unsorted | O(n log n) | The sort dominates completely. For a single lookup an O(n) scan is cheaper. |
06 Where & why
Sorted data, or a boundary question
Binary search is not the fastest lookup you can build — a hash table answers an exact-key question in O(1) and does not care about order at all. What binary search gives you is the class of answers a hash table cannot produce: the first value that is at least x, the position a new value belongs in, the two edges of a repeated run.
Reach for it when…
Use something else when…
| Situation | Better choice | Why |
|---|---|---|
| You only ever look up by exact key, and order never matters | Hash table / dictionary | O(1) average beats O(log n), and you skip the sorting cost entirely. |
| The data is unsorted and you will search it once | Linear search | O(n) beats O(n log n) of sorting plus O(log n) of searching. |
| The elements sit in a linked list | Linear scan, or copy into an array | Reaching mid costs O(n) hops, so every read is O(n) and the log n saving evaporates. |
| Values are inserted and deleted constantly | Balanced BST, skip list, std::set | They keep order under insertion in O(log n); a sorted array costs O(n) to shift on every insert. |
07 Interview questions
What interviewers actually ask
Eleven questions in the order an interview escalates: what it is, how fast, why that fast, and then the duplicate-handling follow-up that decides the round.
What is binary search?
What has to be true about the array before you can run it?
Why is it O(log n) and not O(n)?
Worst case, how many comparisons for 7 elements? For a million?
What is the space complexity?
Why write mid = low + (high − low) / 2 instead of (low + high) / 2?
How do you know the loop always terminates?
On 1 3 4 4 4 7 9, searching for 4 returns index 3. How do you get index 2 instead?
In that tweak, why high = mid − 1 and not high = mid?
How would you count how many times a value appears, in O(log n)?
Binary search or a hash table — when would you actually pick binary search?
08 Practice problems
Six problems, in order of bite
Every one is solvable with only what is above: the inclusive window, the overflow-safe mid, and the record-and-keep-going tweak.