7 Binary Search
Rank 5 · Pattern: halve an ordered search interval · Target: \(O(\log n)\) time, \(O(1)\) space
7.1 Problem definition
Given an integer array nums sorted in ascending order and an integer target, return an index at which target occurs, or the language’s conventional absence result.
7.2 Clarifying questions
- Is the array guaranteed to be sorted, and in which direction?
- Can duplicate values occur? If so, which matching index should I return?
- Can the array be empty or
null?
7.3 Sample answers
- Yes. The array is sorted in ascending order.
- Duplicates may occur, and any matching index is acceptable.
- The array is non-null. An empty array is allowed and should produce
-1in Java/Python orNonein Rust.
7.4 Approach and invariant
Maintain a candidate interval and state its boundary convention before coding.
Use the inclusive interval [low, high]. It is empty when low > high, so the loop uses low <= high and discards the midpoint with mid + 1 or mid - 1.
int mid = low + (high - low) / 2;Use the same inclusive interval [low, high]. Integer floor division computes middle = low + (high - low) // 2.
Use the half-open interval [low, high), with high = nums.len(). It is empty when low == high; discarding the left half uses low = middle + 1, while discarding the right half uses high = middle. This avoids subtracting from an unsigned index.
Invariant: if the target exists and has not been returned, at least one matching index lies in the current candidate interval.
For target 7 in [-4,0,3,7,12]:
| low | high | mid | value | next interval |
|---|---|---|---|---|
| 0 | 4 | 2 | 3 | [3,4] |
| 3 | 4 | 3 | 7 | return 3 |
[-4, 0, 3, 7, 12]
L M H 3 < 7: discard through M
L M H found
7.5 Minimal solution
import java.util.*;
import org.junit.*;
import org.junit.runner.*;
import static org.junit.Assert.*;
public class BinarySearchSolution {
static int search(int[] nums, int target) {
int low = 0;
int high = nums.length - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
int value = nums[mid];
if (value == target) {
return mid;
}
if (value < target) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1;
}
@Test public void emptyArrayHasNoMatch() {
assertEquals(-1, search(new int[0], 2));
}
@Test public void findsOnlyValue() {
assertEquals(0, search(new int[] {2}, 2));
}
@Test public void rejectsAbsentValueFromSingleton() {
assertEquals(-1, search(new int[] {2}, 1));
}
@Test public void findsMiddleValue() {
assertEquals(3, search(new int[] {-4, 0, 3, 7, 12}, 7));
}
@Test public void findsFirstValue() {
assertEquals(0, search(new int[] {1, 3, 5}, 1));
}
@Test public void findsLastValue() {
assertEquals(2, search(new int[] {1, 3, 5}, 5));
}
@Test public void returnsMinusOneForValueBetweenElements() {
assertEquals(-1, search(new int[] {1, 3, 5}, 4));
}
@Test public void returnsMinusOneOutsideArrayRange() {
assertEquals(-1, search(new int[] {1, 3, 5}, 0));
assertEquals(-1, search(new int[] {1, 3, 5}, 6));
}
@Test public void mayReturnAnyDuplicateIndex() {
int index = search(new int[] {1, 2, 2, 2}, 2);
assertTrue(index >= 1 && index <= 3);
}
public static void main(String[] args) {
JUnitCore.main("BinarySearchSolution");
}
}import unittest
def search(nums, target):
low = 0
high = len(nums) - 1
while low <= high:
middle = low + (high - low) // 2
value = nums[middle]
if value == target:
return middle
if value < target:
low = middle + 1
else:
high = middle - 1
return -1
class BinarySearchTests(unittest.TestCase):
def test_empty_array(self):
self.assertEqual(-1, search([], 2))
def test_finds_only_value(self):
self.assertEqual(0, search([2], 2))
def test_rejects_absent_singleton_value(self):
self.assertEqual(-1, search([2], 1))
def test_finds_middle_value(self):
self.assertEqual(3, search([-4, 0, 3, 7, 12], 7))
def test_finds_boundaries(self):
self.assertEqual(0, search([1, 3, 5], 1))
self.assertEqual(2, search([1, 3, 5], 5))
def test_rejects_absent_value(self):
self.assertEqual(-1, search([1, 3, 5], 4))
def test_may_return_any_duplicate_index(self):
self.assertIn(search([1, 2, 2, 2], 2), range(1, 4))
if __name__ == "__main__":
unittest.main()pub fn search(nums: &[i32], target: i32) -> Option<usize> {
let mut low = 0;
let mut high = nums.len();
while low < high {
let middle = low + (high - low) / 2;
if nums[middle] == target {
return Some(middle);
}
if nums[middle] < target {
low = middle + 1;
} else {
high = middle;
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_array() {
assert_eq!(None, search(&[], 2));
}
#[test]
fn finds_only_value() {
assert_eq!(Some(0), search(&[2], 2));
}
#[test]
fn rejects_absent_singleton_value() {
assert_eq!(None, search(&[2], 1));
}
#[test]
fn finds_middle_value() {
assert_eq!(Some(3), search(&[-4, 0, 3, 7, 12], 7));
}
#[test]
fn finds_boundaries() {
assert_eq!(Some(0), search(&[1, 3, 5], 1));
assert_eq!(Some(2), search(&[1, 3, 5], 5));
}
#[test]
fn rejects_absent_value() {
assert_eq!(None, search(&[1, 3, 5], 4));
}
#[test]
fn may_return_any_duplicate_index() {
assert!(matches!(search(&[1, 2, 2, 2], 2), Some(1..=3)));
}
}7.6 High-value tests
| Case | Input / target | Expected |
|---|---|---|
| empty | [] / 4 |
absent |
| one, present | [4] / 4 |
0 |
| one, absent | [4] / 3 |
absent |
| first | [1,3,5] / 1 |
0 |
| last | [1,3,5] / 5 |
2 |
| between values | [1,3,5] / 4 |
absent |
| duplicate contract | [1,2,2,2] / 2 |
any of 1–3 |
7.7 Complexity and correctness
Each comparison removes at least half the remaining candidates, so time is \(O(\log n)\) and iterative space is \(O(1)\).
When the midpoint is smaller than target, sorted order proves every earlier value is also too small; the symmetric argument holds when it is larger. Thus each update preserves the invariant. If the interval becomes empty, no candidate remains.
7.8 If the interviewer pushes
- To find the first occurrence, save a match and continue left, or use a lower-bound search.
- To find an insertion position, return
lowafter the loop. - For a monotonic predicate rather than an array, binary-search the first
truevalue with carefully chosen bounds. - Recursive binary search has the same time but \(O(\log n)\) call-stack space and more boundary plumbing.
7.9 TDD interview script
Confirm the array is non-null and ascending, empty input is allowed, the language’s absence value is defined, and any duplicate match is acceptable. Then write this test list:
- Empty array
- Singleton present
- Singleton absent
- Middle value
- First value
- Last value
- Missing interior value
- Values outside the array range
- Duplicates
Use one red–green–refactor cycle per focused behavior. Run the expected failure, make the smallest boundary update that passes, then rerun the complete example. Avoid testing private pointer movements; the first, last, singleton, and absent cases expose off-by-one errors through public results.
From book/src, run:
./java/run.sh BinarySearchSolution./python/run.sh binary_search_solution./rust/run.sh binary_search_solutionState the interval convention before coding. Java and Python use an inclusive interval here; Rust uses a half-open interval to keep unsigned-index arithmetic simple. Either works, but mixing their update rules creates off-by-one errors.
7.9.1 Pseudocode
low = 0
high = nums.length - 1
while low <= high:
mid = low + (high - low) / 2
if nums[mid] == target:
return mid
if nums[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1
The Java pseudocode uses the same inclusive interval; replace nums.length with len(nums).
low = 0
high = nums.length
while low < high:
middle = low + (high - low) / 2
if nums[middle] == target:
return Some(middle)
if nums[middle] < target:
low = middle + 1
else:
high = middle
return None
The invariant is: if the target exists and has not been returned, at least one matching index remains inside the selected candidate interval.
7.9.2 TDD sequence
| Step | Test code added | Production code added or changed |
|---|---|---|
| 1. Empty interval | emptyArrayHasNoMatch |
Initialize high to length - 1, use low <= high, and return -1 when the interval is empty. |
| 2. Singleton hit | findsOnlyValue |
Compute the midpoint and return it on equality. |
| 3. Singleton miss | rejectsAbsentValueFromSingleton |
Add the less-than and greater-than branches with mid + 1 and mid - 1, guaranteeing progress. |
| 4. Repeated halving | findsMiddleValue |
Loop while candidates remain and retain the appropriate half after each comparison. |
| 5. Left boundary | findsFirstValue |
Verify that an inclusive interval can retain index zero. |
| 6. Right boundary | findsLastValue |
Verify that the final array index is retained and examined. |
| 7. Interior miss | returnsMinusOneForValueBetweenElements |
Confirm that crossing the bounds terminates rather than looping between adjacent values. |
| 8. Exterior misses | returnsMinusOneOutsideArrayRange |
Exercise repeated movement in each direction until no candidates remain. |
| 9. Duplicate contract | mayReturnAnyDuplicateIndex |
Assert only that the returned index is one of the valid duplicate positions; do not over-specify first or last. |
This progression treats interval boundaries as the primary risk. The examples triangulate the inclusive-loop template and the duplicate test preserves freedom that the contract explicitly allows.