Binary search fundamentals
Binary search finds a value in sorted data by repeatedly halving the search window. Each probe discards half of what is left, so a million elements need at most about 20 probes.
Learning goals
- Explain why sorted order is required and what breaks without it
- Write iterative and recursive binary search without off-by-one errors
- Use lower bound and upper bound to answer counting and insertion questions
- Find the first and last occurrence of a value among duplicates
Prerequisites
Finding a word in a paper dictionary: open near the middle, decide whether the word comes before or after, and repeat on that half. You never read every page.
Sorted prerequisite
Binary search relies on one fact: if arr[mid] < target, then every element left of mid is also smaller, so the whole left half can be thrown away. That is only true when the array is sorted.
On unsorted data the algorithm still runs and returns an answer, but the answer can be wrong. It may report -1 for a value that is present.
| Array | Target | Result | Why |
|---|---|---|---|
| [2, 5, 8, 12, 16] | 12 | 3 (correct) | Sorted, so halves can be discarded safely |
| [8, 2, 16, 5, 12] | 5 | -1 (wrong) | mid = 16 > 5 discards the right half, which holds 5 |
Iterative search
Keep a closed window [lo, hi] that must contain the target if it exists. Probe mid, then move lo or hi past mid. The loop ends when the window is empty (lo > hi).
Compute mid as lo + (hi - lo) / 2. The naive (lo + hi) / 2 can overflow int for very large arrays. (lo + hi) >>> 1 is the other safe form.
- Each iteration at least halves the window, so there are at most ⌊log₂ n⌋ + 1 probes.
- The JDK provides
Arrays.binarySearch(arr, key). It returns-(insertionPoint) - 1when the key is missing. - With duplicates, neither this code nor the JDK promises which matching index you get. Use first/last occurrence when it matters.
Best
O(1)
Average
O(log n)
Worst
O(log n)
Space
O(1)
Binary search (iterative)
Interactive · edit the inputint[] arr · length 10
The whole array is the search window: lo = 0, hi = 9.
1public static int binarySearch(int[] arr, int target) {2int lo = 0, hi = arr.length - 1;3while (lo <= hi) {4int mid = lo + (hi - lo) / 2;5if (arr[mid] == target) {6return mid;7} else if (arr[mid] < target) {8lo = mid + 1;9} else {10hi = mid - 1;11}12}13return -1;14}
Live state
target
23
lo
0
hi
9
Execution trace (dry run table)Show 15 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | target=23 lo=0 hi=9 | The whole array is the search window: lo = 0, hi = 9. |
| 2 | L3 | target=23 lo=0 hi=9 | 10 candidates left in [0, 9]. |
| 3 | L4 | target=23 lo=0 hi=9 mid=4 | mid = 0 + (9 - 0) / 2 = 4. Writing it this way avoids int overflow of (lo + hi). |
| 4 | L5 | target=23 lo=0 hi=9 mid=4 arr[mid]=16 | 16 is not 23. Decide which half to throw away. |
| 5 | L7 | target=23 lo=0 hi=9 mid=4 arr[mid]=16 | 16 < 23: everything at or left of mid is too small. |
| 6 | L8 | target=23 lo=5 hi=9 | lo = mid + 1 = 5. The left half is discarded. |
| 7 | L3 | target=23 lo=5 hi=9 | 5 candidates left in [5, 9]. |
| 8 | L4 | target=23 lo=5 hi=9 mid=7 | mid = 5 + (9 - 5) / 2 = 7. Writing it this way avoids int overflow of (lo + hi). |
| 9 | L5 | target=23 lo=5 hi=9 mid=7 arr[mid]=56 | 56 is not 23. Decide which half to throw away. |
| 10 | L7 | target=23 lo=5 hi=9 mid=7 arr[mid]=56 | 56 > 23: everything at or right of mid is too large. |
| 11 | L10 | target=23 lo=5 hi=6 | hi = mid - 1 = 6. The right half is discarded. |
| 12 | L3 | target=23 lo=5 hi=6 | 2 candidates left in [5, 6]. |
| 13 | L4 | target=23 lo=5 hi=6 mid=5 | mid = 5 + (6 - 5) / 2 = 5. Writing it this way avoids int overflow of (lo + hi). |
| 14 | L5 | target=23 lo=5 hi=6 mid=5 arr[mid]=23 | 23 == 23. Match. |
| 15 | L6 | target=23 return=5 | return 5. Found with 3 probes instead of up to 10 for linear search. |
Recursive search
The recursive version expresses the same idea directly: search the left half or the right half. The base case is an empty window.
Each call is a tail call, but Java does not eliminate tail calls, so recursion uses O(log n) stack frames. The animation's call-stack panel shows each frame being pushed and then returning the answer.
Best
O(1)
Average
O(log n)
Worst
O(log n)
Space
O(log n) stack
Binary search (recursive)
Interactive · edit the inputint[] arr · length 9
Enter search(arr, 44, 0, 8). Call depth is 1.
1public static int search(int[] arr, int target, int lo, int hi) {2if (lo > hi) {3return -1;4}5int mid = lo + (hi - lo) / 2;6if (arr[mid] == target) {7return mid;8}9if (arr[mid] < target) {10return search(arr, target, mid + 1, hi);11}12return search(arr, target, lo, mid - 1);13}
Live state
target
44
lo
0
hi
8
depth
1
Call stack (top first)
- search(0, 8)
Execution trace (dry run table)Show 13 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | target=44 lo=0 hi=8 depth=1 | Enter search(arr, 44, 0, 8). Call depth is 1. |
| 2 | L2 | target=44 lo=0 hi=8 depth=1 | Window is not empty, so this call does real work. |
| 3 | L5 | target=44 lo=0 hi=8 mid=4 depth=1 | mid = 4. |
| 4 | L6 | target=44 lo=0 hi=8 mid=4 arr[mid]=27 depth=1 | No match yet. |
| 5 | L9 | target=44 lo=0 hi=8 mid=4 arr[mid]=27 depth=1 | Target is larger, so recurse into the right half. |
| 6 | L10 | target=44 lo=0 hi=8 mid=4 depth=1 | Call search(arr, 44, 5, 8). This call waits for the answer. |
| 7 | L1 | target=44 lo=5 hi=8 depth=2 | Enter search(arr, 44, 5, 8). Call depth is 2. |
| 8 | L2 | target=44 lo=5 hi=8 depth=2 | Window is not empty, so this call does real work. |
| 9 | L5 | target=44 lo=5 hi=8 mid=6 depth=2 | mid = 6. |
| 10 | L6 | target=44 lo=5 hi=8 mid=6 arr[mid]=44 depth=2 | Match. |
| 11 | L7 | target=44 depth=2 return=6 | return 6 from depth 2. |
| 12 | L10 | target=44 depth=1 return=6 | search(5, 8) returned 6. Its caller returns the same value unchanged. |
| 13 | L13 | target=44 return=6 | The original call returns 6. Recursion used 2 calls, so O(log n) stack space. |
Lower bound
Lower bound returns the first index i with arr[i] >= target. If every element is smaller, it returns arr.length. This is exactly where to insert the target to keep the array sorted, before any equal elements.
It uses a half-open window [lo, hi) starting at hi = arr.length. When arr[mid] >= target, mid could be the answer, so we set hi = mid rather than mid - 1.
- Number of elements smaller than x:
lowerBound(arr, x). - Does x exist?
i < n && arr[i] == xwherei = lowerBound(arr, x). - C++
std::lower_boundand JavaTreeSet.ceilinganswer the same question.
Lower bound
Interactive · edit the inputint[] arr · length 8
Find the first index with arr[i] >= 3. hi starts at arr.length (8) because the answer may be "past the end".
1public static int lowerBound(int[] arr, int target) {2int lo = 0, hi = arr.length; // half-open window [lo, hi)3while (lo < hi) {4int mid = lo + (hi - lo) / 2;5if (arr[mid] < target) {6lo = mid + 1;7} else {8hi = mid;9}10}11return lo;12}
Live state
target
3
lo
0
hi
8
Execution trace (dry run table)Show 19 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | target=3 lo=0 hi=8 | Find the first index with arr[i] >= 3. hi starts at arr.length (8) because the answer may be "past the end". |
| 2 | L3 | target=3 lo=0 hi=8 | The answer is somewhere in [0, 8]. |
| 3 | L4 | target=3 lo=0 hi=8 mid=4 | mid = 4. |
| 4 | L5 | target=3 lo=0 hi=8 mid=4 arr[mid]=6 | arr[4] could be the answer. Keep it, but look further left. |
| 5 | L8 | target=3 lo=0 hi=4 | hi = mid = 4. mid stays inside the window as a candidate. |
| 6 | L3 | target=3 lo=0 hi=4 | The answer is somewhere in [0, 4]. |
| 7 | L4 | target=3 lo=0 hi=4 mid=2 | mid = 2. |
| 8 | L5 | target=3 lo=0 hi=4 mid=2 arr[mid]=3 | arr[2] could be the answer. Keep it, but look further left. |
| 9 | L8 | target=3 lo=0 hi=2 | hi = mid = 2. mid stays inside the window as a candidate. |
| 10 | L3 | target=3 lo=0 hi=2 | The answer is somewhere in [0, 2]. |
| 11 | L4 | target=3 lo=0 hi=2 mid=1 | mid = 1. |
| 12 | L5 | target=3 lo=0 hi=2 mid=1 arr[mid]=3 | arr[1] could be the answer. Keep it, but look further left. |
| 13 | L8 | target=3 lo=0 hi=1 | hi = mid = 1. mid stays inside the window as a candidate. |
| 14 | L3 | target=3 lo=0 hi=1 | The answer is somewhere in [0, 1]. |
| 15 | L4 | target=3 lo=0 hi=1 mid=0 | mid = 0. |
| 16 | L5 | target=3 lo=0 hi=1 mid=0 arr[mid]=1 | arr[0] cannot be the answer, nor anything to its left. |
| 17 | L6 | target=3 lo=1 hi=1 | lo = 1. |
| 18 | L3 | target=3 lo=1 hi=1 | lo == hi == 1: the window has collapsed onto the answer. |
| 19 | L11 | target=3 return=1 | return 1: arr[1] = 3 is the first index with arr[i] >= 3. Inserting 3 here keeps the array sorted. |
Upper bound
Upper bound returns the first index i with arr[i] > target. The only change from lower bound is < becoming <= in the comparison.
Together the two bounds answer counting questions in O(log n): the number of copies of x is upperBound(x) - lowerBound(x), and the number of values in [a, b] is upperBound(b) - lowerBound(a).
Upper bound
Interactive · edit the inputint[] arr · length 8
Find the first index with arr[i] > 3. hi starts at arr.length (8) because the answer may be "past the end".
1public static int upperBound(int[] arr, int target) {2int lo = 0, hi = arr.length; // half-open window [lo, hi)3while (lo < hi) {4int mid = lo + (hi - lo) / 2;5if (arr[mid] <= target) {6lo = mid + 1;7} else {8hi = mid;9}10}11return lo;12}
Live state
target
3
lo
0
hi
8
Execution trace (dry run table)Show 15 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | target=3 lo=0 hi=8 | Find the first index with arr[i] > 3. hi starts at arr.length (8) because the answer may be "past the end". |
| 2 | L3 | target=3 lo=0 hi=8 | The answer is somewhere in [0, 8]. |
| 3 | L4 | target=3 lo=0 hi=8 mid=4 | mid = 4. |
| 4 | L5 | target=3 lo=0 hi=8 mid=4 arr[mid]=6 | arr[4] could be the answer. Keep it, but look further left. |
| 5 | L8 | target=3 lo=0 hi=4 | hi = mid = 4. mid stays inside the window as a candidate. |
| 6 | L3 | target=3 lo=0 hi=4 | The answer is somewhere in [0, 4]. |
| 7 | L4 | target=3 lo=0 hi=4 mid=2 | mid = 2. |
| 8 | L5 | target=3 lo=0 hi=4 mid=2 arr[mid]=3 | arr[2] cannot be the answer, nor anything to its left. |
| 9 | L6 | target=3 lo=3 hi=4 | lo = 3. |
| 10 | L3 | target=3 lo=3 hi=4 | The answer is somewhere in [3, 4]. |
| 11 | L4 | target=3 lo=3 hi=4 mid=3 | mid = 3. |
| 12 | L5 | target=3 lo=3 hi=4 mid=3 arr[mid]=3 | arr[3] cannot be the answer, nor anything to its left. |
| 13 | L6 | target=3 lo=4 hi=4 | lo = 4. |
| 14 | L3 | target=3 lo=4 hi=4 | lo == hi == 4: the window has collapsed onto the answer. |
| 15 | L11 | target=3 return=4 | return 4: arr[4] = 6 is the first index with arr[i] > 3. Count of elements <= 3 is 4. |
First/last occurrence
To find the first copy of a value, do not stop at a match. Record it in ans and keep searching left. For the last copy, record it and keep searching right.
Both remain O(log n) even when every element is the same value. A linear scan outward from any match would degrade to O(n).
First occurrence
Interactive · edit the inputint[] arr · length 9
ans = -1 until we see a match. Ordinary binary search would stop at the first match; this one keeps going left.
1public static int firstOccurrence(int[] arr, int target) {2int lo = 0, hi = arr.length - 1, ans = -1;3while (lo <= hi) {4int mid = lo + (hi - lo) / 2;5if (arr[mid] == target) {6ans = mid; // remember this match7hi = mid - 1; // keep looking left8} else if (arr[mid] < target) {9lo = mid + 1;10} else {11hi = mid - 1;12}13}14return ans;15}
Live state
target
4
lo
0
hi
8
ans
-1
Execution trace (dry run table)Show 18 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | target=4 lo=0 hi=8 ans=-1 | ans = -1 until we see a match. Ordinary binary search would stop at the first match; this one keeps going left. |
| 2 | L3 | target=4 lo=0 hi=8 ans=-1 | Search [0, 8]. |
| 3 | L4 | target=4 lo=0 hi=8 ans=-1 mid=4 | mid = 4. |
| 4 | L5 | target=4 lo=0 hi=8 ans=-1 mid=4 arr[mid]=4 | A match, but maybe not the first one. |
| 5 | L6 | target=4 lo=0 hi=8 ans=4 mid=4 | ans = 4. This is the best answer so far. |
| 6 | L7 | target=4 lo=0 hi=3 ans=4 | hi = 3. An earlier copy could still exist on the left. |
| 7 | L3 | target=4 lo=0 hi=3 ans=4 | Search [0, 3]. |
| 8 | L4 | target=4 lo=0 hi=3 ans=4 mid=1 | mid = 1. |
| 9 | L5 | target=4 lo=0 hi=3 ans=4 mid=1 arr[mid]=2 | Not equal. |
| 10 | L8 | target=4 lo=0 hi=3 ans=4 mid=1 arr[mid]=2 | Too small: go right. |
| 11 | L9 | target=4 lo=2 hi=3 ans=4 | lo = 2. |
| 12 | L3 | target=4 lo=2 hi=3 ans=4 | Search [2, 3]. |
| 13 | L4 | target=4 lo=2 hi=3 ans=4 mid=2 | mid = 2. |
| 14 | L5 | target=4 lo=2 hi=3 ans=4 mid=2 arr[mid]=4 | A match, but maybe not the first one. |
| 15 | L6 | target=4 lo=2 hi=3 ans=2 mid=2 | ans = 2. This is the best answer so far. |
| 16 | L7 | target=4 lo=2 hi=1 ans=2 | hi = 1. An earlier copy could still exist on the left. |
| 17 | L3 | target=4 lo=2 hi=1 ans=2 | Window empty. |
| 18 | L14 | target=4 return=2 | return 2: the first index holding 4. |
Last occurrence
Interactive · edit the inputint[] arr · length 9
ans = -1 until we see a match. Ordinary binary search would stop at the first match; this one keeps going right.
1public static int lastOccurrence(int[] arr, int target) {2int lo = 0, hi = arr.length - 1, ans = -1;3while (lo <= hi) {4int mid = lo + (hi - lo) / 2;5if (arr[mid] == target) {6ans = mid; // remember this match7lo = mid + 1; // keep looking right8} else if (arr[mid] < target) {9lo = mid + 1;10} else {11hi = mid - 1;12}13}14return ans;15}
Live state
target
4
lo
0
hi
8
ans
-1
Execution trace (dry run table)Show 18 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | target=4 lo=0 hi=8 ans=-1 | ans = -1 until we see a match. Ordinary binary search would stop at the first match; this one keeps going right. |
| 2 | L3 | target=4 lo=0 hi=8 ans=-1 | Search [0, 8]. |
| 3 | L4 | target=4 lo=0 hi=8 ans=-1 mid=4 | mid = 4. |
| 4 | L5 | target=4 lo=0 hi=8 ans=-1 mid=4 arr[mid]=4 | A match, but maybe not the last one. |
| 5 | L6 | target=4 lo=0 hi=8 ans=4 mid=4 | ans = 4. This is the best answer so far. |
| 6 | L7 | target=4 lo=5 hi=8 ans=4 | lo = 5. A later copy could still exist on the right. |
| 7 | L3 | target=4 lo=5 hi=8 ans=4 | Search [5, 8]. |
| 8 | L4 | target=4 lo=5 hi=8 ans=4 mid=6 | mid = 6. |
| 9 | L5 | target=4 lo=5 hi=8 ans=4 mid=6 arr[mid]=4 | A match, but maybe not the last one. |
| 10 | L6 | target=4 lo=5 hi=8 ans=6 mid=6 | ans = 6. This is the best answer so far. |
| 11 | L7 | target=4 lo=7 hi=8 ans=6 | lo = 7. A later copy could still exist on the right. |
| 12 | L3 | target=4 lo=7 hi=8 ans=6 | Search [7, 8]. |
| 13 | L4 | target=4 lo=7 hi=8 ans=6 mid=7 | mid = 7. |
| 14 | L5 | target=4 lo=7 hi=8 ans=6 mid=7 arr[mid]=7 | Not equal. |
| 15 | L8 | target=4 lo=7 hi=8 ans=6 mid=7 arr[mid]=7 | Too large: go left. |
| 16 | L11 | target=4 lo=7 hi=6 ans=6 | hi = 6. |
| 17 | L3 | target=4 lo=7 hi=6 ans=6 | Window empty. |
| 18 | L14 | target=4 return=6 | return 6: the last index holding 4. |
Practice ladder
Now try it yourself
Easy
- Search insert position
- Count occurrences of x in a sorted array
- Check if a number is a perfect square without Math.sqrt
Medium
- Find first and last position of an element
- Search a 2D matrix whose rows are sorted and chained
- Find the smallest letter greater than a target
Advanced
- Median of two sorted arrays in O(log(min(m, n)))
- Count values in [a, b] for many queries
- Implement Arrays.binarySearch semantics including the negative insertion point
Key takeaways
- Binary search needs sorted data and runs in O(log n).
- Pick one window convention, closed [lo, hi] or half-open [lo, hi), and keep the update rules consistent with it.
- Lower and upper bound turn search into counting and insertion tools.