Chapter 16: Searching Algorithms
16.02Beginner / Intermediate6 topics20 min5 interactive animations

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

Linear searchSorted arraysRecursion basicsInteger division

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.

16.02.1

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.

ArrayTargetResultWhy
[2, 5, 8, 12, 16]123 (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
16.02.4

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] == x where i = lowerBound(arr, x).
  • C++ std::lower_bound and Java TreeSet.ceiling answer the same question.

Lower bound

Interactive · edit the input

Your input

Try

int[] arr · length 8

1
3
3
3
6
8
8
11
012345678
Being comparedNot a matchDiscardedMatch / answer
Setup

Find the first index with arr[i] >= 3. hi starts at arr.length (8) because the answer may be "past the end".

Step 1 / 19
Java
1public static int lowerBound(int[] arr, int target) {
2 int lo = 0, hi = arr.length; // half-open window [lo, hi)
3 while (lo < hi) {
4 int mid = lo + (hi - lo) / 2;
5 if (arr[mid] < target) {
6 lo = mid + 1;
7 } else {
8 hi = mid;
9 }
10 }
11 return lo;
12}

Live state

0 probes

target

3

lo

0

hi

8

No condition evaluated on this step
Execution trace (dry run table)Show 19 steps
StepLineStateWhat happens
1L2target=3 lo=0 hi=8Find the first index with arr[i] >= 3. hi starts at arr.length (8) because the answer may be "past the end".
2L3target=3 lo=0 hi=8The answer is somewhere in [0, 8].
3L4target=3 lo=0 hi=8 mid=4mid = 4.
4L5target=3 lo=0 hi=8 mid=4 arr[mid]=6arr[4] could be the answer. Keep it, but look further left.
5L8target=3 lo=0 hi=4hi = mid = 4. mid stays inside the window as a candidate.
6L3target=3 lo=0 hi=4The answer is somewhere in [0, 4].
7L4target=3 lo=0 hi=4 mid=2mid = 2.
8L5target=3 lo=0 hi=4 mid=2 arr[mid]=3arr[2] could be the answer. Keep it, but look further left.
9L8target=3 lo=0 hi=2hi = mid = 2. mid stays inside the window as a candidate.
10L3target=3 lo=0 hi=2The answer is somewhere in [0, 2].
11L4target=3 lo=0 hi=2 mid=1mid = 1.
12L5target=3 lo=0 hi=2 mid=1 arr[mid]=3arr[1] could be the answer. Keep it, but look further left.
13L8target=3 lo=0 hi=1hi = mid = 1. mid stays inside the window as a candidate.
14L3target=3 lo=0 hi=1The answer is somewhere in [0, 1].
15L4target=3 lo=0 hi=1 mid=0mid = 0.
16L5target=3 lo=0 hi=1 mid=0 arr[mid]=1arr[0] cannot be the answer, nor anything to its left.
17L6target=3 lo=1 hi=1lo = 1.
18L3target=3 lo=1 hi=1lo == hi == 1: the window has collapsed onto the answer.
19L11target=3 return=1return 1: arr[1] = 3 is the first index with arr[i] >= 3. Inserting 3 here keeps the array sorted.
16.02.5

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 input

Your input

Try

int[] arr · length 8

1
3
3
3
6
8
8
11
012345678
Being comparedNot a matchDiscardedMatch / answer
Setup

Find the first index with arr[i] > 3. hi starts at arr.length (8) because the answer may be "past the end".

Step 1 / 15
Java
1public static int upperBound(int[] arr, int target) {
2 int lo = 0, hi = arr.length; // half-open window [lo, hi)
3 while (lo < hi) {
4 int mid = lo + (hi - lo) / 2;
5 if (arr[mid] <= target) {
6 lo = mid + 1;
7 } else {
8 hi = mid;
9 }
10 }
11 return lo;
12}

Live state

0 probes

target

3

lo

0

hi

8

No condition evaluated on this step
Execution trace (dry run table)Show 15 steps
StepLineStateWhat happens
1L2target=3 lo=0 hi=8Find the first index with arr[i] > 3. hi starts at arr.length (8) because the answer may be "past the end".
2L3target=3 lo=0 hi=8The answer is somewhere in [0, 8].
3L4target=3 lo=0 hi=8 mid=4mid = 4.
4L5target=3 lo=0 hi=8 mid=4 arr[mid]=6arr[4] could be the answer. Keep it, but look further left.
5L8target=3 lo=0 hi=4hi = mid = 4. mid stays inside the window as a candidate.
6L3target=3 lo=0 hi=4The answer is somewhere in [0, 4].
7L4target=3 lo=0 hi=4 mid=2mid = 2.
8L5target=3 lo=0 hi=4 mid=2 arr[mid]=3arr[2] cannot be the answer, nor anything to its left.
9L6target=3 lo=3 hi=4lo = 3.
10L3target=3 lo=3 hi=4The answer is somewhere in [3, 4].
11L4target=3 lo=3 hi=4 mid=3mid = 3.
12L5target=3 lo=3 hi=4 mid=3 arr[mid]=3arr[3] cannot be the answer, nor anything to its left.
13L6target=3 lo=4 hi=4lo = 4.
14L3target=3 lo=4 hi=4lo == hi == 4: the window has collapsed onto the answer.
15L11target=3 return=4return 4: arr[4] = 6 is the first index with arr[i] > 3. Count of elements <= 3 is 4.
16.02.6

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 input

Your input

Try

int[] arr · length 9

1
2
4
4
4
4
4
7
9
012345678
Being comparedNot a matchCandidateDiscardedMatch / answer
Setup

ans = -1 until we see a match. Ordinary binary search would stop at the first match; this one keeps going left.

Step 1 / 18
Java
1public static int firstOccurrence(int[] arr, int target) {
2 int lo = 0, hi = arr.length - 1, ans = -1;
3 while (lo <= hi) {
4 int mid = lo + (hi - lo) / 2;
5 if (arr[mid] == target) {
6 ans = mid; // remember this match
7 hi = mid - 1; // keep looking left
8 } else if (arr[mid] < target) {
9 lo = mid + 1;
10 } else {
11 hi = mid - 1;
12 }
13 }
14 return ans;
15}

Live state

0 probes

target

4

lo

0

hi

8

ans

-1

No condition evaluated on this step
Execution trace (dry run table)Show 18 steps
StepLineStateWhat happens
1L2target=4 lo=0 hi=8 ans=-1ans = -1 until we see a match. Ordinary binary search would stop at the first match; this one keeps going left.
2L3target=4 lo=0 hi=8 ans=-1Search [0, 8].
3L4target=4 lo=0 hi=8 ans=-1 mid=4mid = 4.
4L5target=4 lo=0 hi=8 ans=-1 mid=4 arr[mid]=4A match, but maybe not the first one.
5L6target=4 lo=0 hi=8 ans=4 mid=4ans = 4. This is the best answer so far.
6L7target=4 lo=0 hi=3 ans=4hi = 3. An earlier copy could still exist on the left.
7L3target=4 lo=0 hi=3 ans=4Search [0, 3].
8L4target=4 lo=0 hi=3 ans=4 mid=1mid = 1.
9L5target=4 lo=0 hi=3 ans=4 mid=1 arr[mid]=2Not equal.
10L8target=4 lo=0 hi=3 ans=4 mid=1 arr[mid]=2Too small: go right.
11L9target=4 lo=2 hi=3 ans=4lo = 2.
12L3target=4 lo=2 hi=3 ans=4Search [2, 3].
13L4target=4 lo=2 hi=3 ans=4 mid=2mid = 2.
14L5target=4 lo=2 hi=3 ans=4 mid=2 arr[mid]=4A match, but maybe not the first one.
15L6target=4 lo=2 hi=3 ans=2 mid=2ans = 2. This is the best answer so far.
16L7target=4 lo=2 hi=1 ans=2hi = 1. An earlier copy could still exist on the left.
17L3target=4 lo=2 hi=1 ans=2Window empty.
18L14target=4 return=2return 2: the first index holding 4.

Last occurrence

Interactive · edit the input

Your input

Try

int[] arr · length 9

1
2
4
4
4
4
4
7
9
012345678
Being comparedNot a matchCandidateDiscardedMatch / answer
Setup

ans = -1 until we see a match. Ordinary binary search would stop at the first match; this one keeps going right.

Step 1 / 18
Java
1public static int lastOccurrence(int[] arr, int target) {
2 int lo = 0, hi = arr.length - 1, ans = -1;
3 while (lo <= hi) {
4 int mid = lo + (hi - lo) / 2;
5 if (arr[mid] == target) {
6 ans = mid; // remember this match
7 lo = mid + 1; // keep looking right
8 } else if (arr[mid] < target) {
9 lo = mid + 1;
10 } else {
11 hi = mid - 1;
12 }
13 }
14 return ans;
15}

Live state

0 probes

target

4

lo

0

hi

8

ans

-1

No condition evaluated on this step
Execution trace (dry run table)Show 18 steps
StepLineStateWhat happens
1L2target=4 lo=0 hi=8 ans=-1ans = -1 until we see a match. Ordinary binary search would stop at the first match; this one keeps going right.
2L3target=4 lo=0 hi=8 ans=-1Search [0, 8].
3L4target=4 lo=0 hi=8 ans=-1 mid=4mid = 4.
4L5target=4 lo=0 hi=8 ans=-1 mid=4 arr[mid]=4A match, but maybe not the last one.
5L6target=4 lo=0 hi=8 ans=4 mid=4ans = 4. This is the best answer so far.
6L7target=4 lo=5 hi=8 ans=4lo = 5. A later copy could still exist on the right.
7L3target=4 lo=5 hi=8 ans=4Search [5, 8].
8L4target=4 lo=5 hi=8 ans=4 mid=6mid = 6.
9L5target=4 lo=5 hi=8 ans=4 mid=6 arr[mid]=4A match, but maybe not the last one.
10L6target=4 lo=5 hi=8 ans=6 mid=6ans = 6. This is the best answer so far.
11L7target=4 lo=7 hi=8 ans=6lo = 7. A later copy could still exist on the right.
12L3target=4 lo=7 hi=8 ans=6Search [7, 8].
13L4target=4 lo=7 hi=8 ans=6 mid=7mid = 7.
14L5target=4 lo=7 hi=8 ans=6 mid=7 arr[mid]=7Not equal.
15L8target=4 lo=7 hi=8 ans=6 mid=7 arr[mid]=7Too large: go left.
16L11target=4 lo=7 hi=6 ans=6hi = 6.
17L3target=4 lo=7 hi=6 ans=6Window empty.
18L14target=4 return=6return 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.