Part 6 · Lesson 28 · 26 min read

Binary Search on a Sorted Array

Halve the search space every step; first and last positions; rotated arrays.

On this page

  1. Halve the search space every step
  2. The binary search template
  3. Traced: searching for 9
  4. Infinite loops and off-by-one errors
  5. Lower bound and upper bound
  6. First and last occurrence
  7. The general idea: find the first “true”
  8. Rotated sorted arrays
  9. Practice questions (7)
  10. Make it stick
  11. What's next

Halve the search space every step

Think of a number between 1 and 100, and I will guess it. After each guess you only say “higher” or “lower”. The best plan is to guess the middle each time: 50, then 25 or 75, and so on. Every answer removes half of the remaining possibilities. So 7 guesses are always enough (27 = 128, which is more than 100).

Binary search is a method that finds a value in a sorted array by checking the middle item and throwing away half of the array each time. Compare the target with the middle item. If the middle item is too small, the target can only be in the right half. If it is too big, the target can only be in the left half. Halving again and again (the loop from Lesson 12) makes the time O(log n). This means about 20 steps for a million items and about 30 steps for a billion items. (log n is the number of times you can halve n until you reach 1.)

The binary search template

Keep two indices (positions), lo and hi. They mark the range where the target could still be. Each step looks at the middle and throws away the half that cannot contain the target.

JavaScript
function binarySearch(nums, target) {  let lo = 0, hi = nums.length - 1;          // search the closed range [lo, hi]  while (lo <= hi) {                          // non-empty while lo <= hi    const mid = lo + Math.floor((hi - lo) / 2);    if (nums[mid] === target) return mid;    if (nums[mid] < target) lo = mid + 1;     // mid is ruled out, so skip it    else hi = mid - 1;  }  return -1;}console.log(binarySearch([-1, 0, 3, 5, 9, 12], 9)); // 4console.log(binarySearch([-1, 0, 3, 5, 9, 12], 2)); // -1

Traced: searching for 9

step 1 / 11
Set up
lo = 0, hi = 6

If the answer exists, it is somewhere in nums[lo..hi]. For now that is the whole array.

1const nums = [1, 3, 5, 7, 9, 11, 13];2const target = 9;▶let lo = 0, hi = nums.length - 1;4while (lo <= hi) {5  const mid = Math.floor((lo + hi) / 2);6  if (nums[mid] === target) break;7  if (nums[mid] < target) lo = mid + 1;8  else hi = mid - 1;9}10console.log(lo <= hi ? "found" : "missing");
Variables
nums[1, 3, 5, 7, 9, 11, 13]
target9
lo0
hi6
Console
(nothing printed yet)
Each comparison discards half of what is left. If the target exists, it is always still inside the range [lo, hi]. · interactive
nums =
1
0
3
1
5
2
mid 1
7
3
mid 3
9
4
mid 2
11
5
13
6
↑ index (position), always starting at 0
The three middles checked: index 3 (7), index 5 (11), index 4 (9).

Infinite loops and off-by-one errors

Binary search is short, but it is easy to get wrong. An off-by-one error means a boundary is wrong by exactly one position. An infinite loop is a loop that never ends. Most bugs come from mixing two styles. Choose one style and keep its three parts (start, loop condition, moves) consistent. A closed range [lo, hi] includes both ends. A half-open range [lo, hi) includes lo but not hi.

StyleStartLoop whileMovesUse for
Closed [lo, hi]hi = n − 1lo <= hilo = mid + 1, hi = mid − 1Finding an exact value
Half-open [lo, hi)hi = nlo < hilo = mid + 1, hi = midFinding a boundary (first index where…)
The infinite-loop check

Every step must make the range smaller. Take the half-open style with lo < hi and mid = floor((lo + hi) / 2). Here mid is always less than hi. So hi = mid makes the range smaller, and lo = mid + 1 does too. If you write lo = mid in that style, the loop can run forever when two items are left. When in doubt, dry-run your code with two items.

Writing lo + Math.floor((hi - lo) / 2) instead of (lo + hi) / 2 avoids overflow. Overflow means a number gets too big for the space that holds it. It happens in languages with fixed-size integers, such as Java or C++. JavaScript numbers do not overflow at this size, but many programmers use this habit and interviewers recognise it.

Lower bound and upper bound

Often you do not need “is it there?”. You need “where does it start?”. The lower bound is the first index whose value is ≥ target. The upper bound is the first index whose value is > target. Both use the half-open style. When mid might be the answer, keep it with hi = mid.

JavaScript
// First index i with nums[i] >= target (nums.length if none).function lowerBound(nums, target) {  let lo = 0, hi = nums.length;               // half-open range [lo, hi)  while (lo < hi) {    const mid = lo + Math.floor((hi - lo) / 2);    if (nums[mid] < target) lo = mid + 1;     // mid is too small, so the answer is right of it    else hi = mid;                            // mid might be the answer, so keep it  }  return lo;}// First index i with nums[i] > target.function upperBound(nums, target) {  let lo = 0, hi = nums.length;  while (lo < hi) {    const mid = lo + Math.floor((hi - lo) / 2);    if (nums[mid] <= target) lo = mid + 1;    else hi = mid;  }  return lo;}const a = [1, 2, 2, 2, 5, 7];console.log(lowerBound(a, 2), upperBound(a, 2)); // 1 4console.log(lowerBound(a, 3), upperBound(a, 3)); // 4 4   (3 would be inserted at 4)console.log(upperBound(a, 2) - lowerBound(a, 2)); // 3   how many 2s
Dry run · bounds in [1, 2, 2, 2, 5, 7]
targetlower boundupper boundmeaning
214the 2s are at indices 1..3
344not present; insert at index 4
000smaller than everything
966larger than everything (n)

The lower bound is also the answer to “search insert position”. This is the index where the value would go to keep the array sorted.

First and last occurrence

When the array has duplicates, the first occurrence of x is at lowerBound(x) (check that this index really holds x). The last occurrence is at upperBound(x) − 1. The count of x is upperBound(x) − lowerBound(x). This uses two O(log n) searches instead of an O(n) scan.

The general idea: find the first “true”

Lower bound and upper bound are the same algorithm with a different test. A predicate is a function that answers yes or no (true or false). Binary search works whenever a predicate about index i is false for a while and then true for the rest, like false, false, false, true, true. Binary search finds the first true:

JavaScript
// The first index where check(i) becomes true, when check gives false…false, true…true.function firstTrue(lo, hi, check) {          // searches [lo, hi); returns hi if never true  while (lo < hi) {    const mid = lo + Math.floor((hi - lo) / 2);    if (check(mid)) hi = mid;    else lo = mid + 1;  }  return lo;}const nums = [1, 2, 2, 2, 5, 7];console.log(firstTrue(0, nums.length, (i) => nums[i] >= 2)); // 1   lower bound of 2console.log(firstTrue(0, nums.length, (i) => nums[i] > 2));  // 4   upper bound of 2

This view makes binary search very powerful. The “array” does not even need to exist. Lesson 29 searches over possible answers instead of indices.

Rotated sorted arrays

A rotated sorted array is a sorted array whose front part was moved to the back, at a point you do not know. An example is [4, 5, 6, 7, 0, 1, 2]. The whole array is not sorted. But when you cut it at any mid, at least one half is sorted (this lesson assumes all values are different). Check which half is sorted by comparing nums[lo] with nums[mid]. If the target lies inside the range of that sorted half, search that half. Otherwise search the other half.

JavaScript
function search(nums, target) {  let lo = 0, hi = nums.length - 1;  while (lo <= hi) {    const mid = lo + Math.floor((hi - lo) / 2);    if (nums[mid] === target) return mid;    if (nums[lo] <= nums[mid]) {                        // left half lo..mid is sorted      if (nums[lo] <= target && target < nums[mid]) hi = mid - 1;      else lo = mid + 1;    } else {                                            // right half mid..hi is sorted      if (nums[mid] < target && target <= nums[hi]) lo = mid + 1;      else hi = mid - 1;    }  }  return -1;}console.log(search([4, 5, 6, 7, 0, 1, 2], 0)); // 4console.log(search([4, 5, 6, 7, 0, 1, 2], 3)); // -1
Dry run · search([4, 5, 6, 7, 0, 1, 2], 0)
lohimidsorted half0 inside it?next
063 (7)left: 4..7nolo = 4
465 (1)left: 0..1yeshi = 4
444 (0)—foundreturn 4

Practice questions

For each question, first write the yes/no test that changes from false to true. The code then follows from it.

Q1Binary searchEasy

Return the index of target in the sorted array, or −1. Use O(log n) time.

Example 1
Input: nums = [-1, 0, 3, 5, 9, 12], target = 9
Output: 4
Explanation: 9 is at index 4.
Example 2
Input: nums = [-1, 0, 3, 5, 9, 12], target = 2
Output: -1
Explanation: Not present.
Stuck? Open one hint at a time
Hint 1
Closed range [lo, hi], loop while lo <= hi.
Show the solution with explanations
Approach 1Closed-range template
The idea

The template from the lesson.

JavaScript
function search(nums, target) {  let lo = 0, hi = nums.length - 1;  while (lo <= hi) {    const mid = lo + Math.floor((hi - lo) / 2);    if (nums[mid] === target) return mid;    if (nums[mid] < target) lo = mid + 1;    else hi = mid - 1;  }  return -1;}console.log(search([-1, 0, 3, 5, 9, 12], 9)); // 4console.log(search([-1, 0, 3, 5, 9, 12], 2)); // -1
How it works

O(log n) time, O(1) extra space.

Which approach should you use?

Practise writing it until you can do it without thinking. (LeetCode 704.)

Q2Search insert positionEasy

Return the index of target if found; otherwise the index where it would be inserted to keep the array sorted.

Example 1
Input: nums = [1, 3, 5, 6], target = 5
Output: 2
Explanation: Found at index 2.
Example 2
Input: nums = [1, 3, 5, 6], target = 2
Output: 1
Explanation: 2 would go between 1 and 3.
Example 3
Input: nums = [1, 3, 5, 6], target = 7
Output: 4
Explanation: After everything.
Stuck? Open one hint at a time
Hint 1
This is the lower bound: the first index whose value is ≥ target.
Show the solution with explanations
Approach 1Lower bound
The idea

Half-open binary search for the first value ≥ target.

JavaScript
function searchInsert(nums, target) {  let lo = 0, hi = nums.length;  while (lo < hi) {    const mid = lo + Math.floor((hi - lo) / 2);    if (nums[mid] < target) lo = mid + 1;    else hi = mid;  }  return lo;}console.log(searchInsert([1, 3, 5, 6], 5)); // 2console.log(searchInsert([1, 3, 5, 6], 2)); // 1console.log(searchInsert([1, 3, 5, 6], 7)); // 4
How it works

O(log n) time. Starting hi at nums.length (not length − 1) is what allows the answer “after everything”.

Which approach should you use?

(LeetCode 35.)

Q3First and last positionMedium

Return the first and last index of target in a sorted array, or [−1, −1], in O(log n).

Example 1
Input: nums = [5, 7, 7, 8, 8, 10], target = 8
Output: [3, 4]
Explanation: The 8s are at indices 3 and 4.
Example 2
Input: nums = [5, 7, 7, 8, 8, 10], target = 6
Output: [-1, -1]
Explanation: Not present.
Example 3
Input: nums = [], target = 0
Output: [-1, -1]
Explanation: Edge case: empty.
Stuck? Open one hint at a time
Hint 1
The lower bound gives the first index. The upper bound minus 1 gives the last index.
Show all 2 solutions with explanations
Approach 1Find first, then scan right
The idea

Find the first 8 with indexOf (a simple scan), then walk right to the last 8.

JavaScript
function searchRange(nums, target) {  const i = nums.indexOf(target);  if (i === -1) return [-1, -1];  let j = i;  while (j + 1 < nums.length && nums[j + 1] === target) j++;  return [i, j];}console.log(searchRange([5, 7, 7, 8, 8, 10], 8)); // [ 3, 4 ]
How it works

O(n) time in the worst case (when all values are equal). The problem asks for O(log n), so this is not enough.

Approach 2Two binary searches
The idea

Use lowerBound(target) for the first index and upperBound(target) − 1 for the last index.

JavaScript
function searchRange(nums, target) {  const bound = (strict) => {    let lo = 0, hi = nums.length;    while (lo < hi) {      const mid = lo + Math.floor((hi - lo) / 2);      if (nums[mid] < target || (strict && nums[mid] === target)) lo = mid + 1;      else hi = mid;    }    return lo;  };  const first = bound(false);  if (first === nums.length || nums[first] !== target) return [-1, -1];  return [first, bound(true) - 1];}console.log(searchRange([5, 7, 7, 8, 8, 10], 8)); // [ 3, 4 ]console.log(searchRange([5, 7, 7, 8, 8, 10], 6)); // [ -1, -1 ]console.log(searchRange([], 0));                  // [ -1, -1 ]
How it works

O(log n) time. One helper function with a flag gives both bounds.

Which approach should you use?

This is a very common medium question. It checks whether you can change the template to find boundaries. (LeetCode 34.)

Q4First bad versionEasy

There are versions 1 to n. From some version on, all versions are bad. Return the first bad version, and call isBad(v) as few times as possible.

Example 1
Input: n = 5, first bad = 4
Output: 4
Explanation: isBad(3) is false, isBad(4) is true; every version after a bad one is also bad.
Stuck? Open one hint at a time
Hint 1
isBad(i) gives false…false, true…true. Find the first true.
Show the solution with explanations
Approach 1First true
The idea

This is binary search with a yes/no test (a predicate), over versions 1…n.

JavaScript
function firstBadVersion(n, isBad) {  let lo = 1, hi = n;  while (lo < hi) {    const mid = lo + Math.floor((hi - lo) / 2);    if (isBad(mid)) hi = mid;      // mid might be the first bad one    else lo = mid + 1;             // mid is good: the first bad one is later  }  return lo;}console.log(firstBadVersion(5, (v) => v >= 4));          // 4console.log(firstBadVersion(2126753390, (v) => v >= 1702766719)); // 1702766719
How it works

O(log n) calls to isBad. That is about 31 calls for n ≈ 2 × 109. There is no array here at all, only a yes/no question.

Which approach should you use?

This is the clearest example of “find the first true”. (LeetCode 278.)

Q5Search in a rotated sorted arrayMedium

A sorted array of distinct values was rotated at an unknown index. Return the index of target, or −1, in O(log n).

Example 1
Input: nums = [4, 5, 6, 7, 0, 1, 2], target = 0
Output: 4
Explanation: See the dry run in the lesson.
Example 2
Input: nums = [4, 5, 6, 7, 0, 1, 2], target = 3
Output: -1
Explanation: Not present.
Example 3
Input: nums = [1], target = 0
Output: -1
Explanation: Edge case.
Stuck? Open one hint at a time
Hint 1
At least one half next to mid is sorted. Decide whether the target lies in that sorted half.
Show the solution with explanations
Approach 1Sorted-half test
The idea

The method from the lesson.

JavaScript
function search(nums, target) {  let lo = 0, hi = nums.length - 1;  while (lo <= hi) {    const mid = lo + Math.floor((hi - lo) / 2);    if (nums[mid] === target) return mid;    if (nums[lo] <= nums[mid]) {      if (nums[lo] <= target && target < nums[mid]) hi = mid - 1;      else lo = mid + 1;    } else {      if (nums[mid] < target && target <= nums[hi]) lo = mid + 1;      else hi = mid - 1;    }  }  return -1;}console.log(search([4, 5, 6, 7, 0, 1, 2], 0)); // 4console.log(search([4, 5, 6, 7, 0, 1, 2], 3)); // -1console.log(search([1], 0));                   // -1
How it works

O(log n) time. The <= in nums[lo] <= nums[mid] handles the case lo = mid (a left half with one item).

Which approach should you use?

(LeetCode 33.)

Q6Minimum in a rotated sorted arrayMedium

Return the smallest value of a rotated sorted array of distinct values, in O(log n).

Example 1
Input: [3, 4, 5, 1, 2]
Output: 1
Explanation: The rotation point.
Example 2
Input: [4, 5, 6, 7, 0, 1, 2]
Output: 0
Explanation: The drop from 7 to 0 marks the minimum.
Example 3
Input: [11, 13, 15, 17]
Output: 11
Explanation: Edge case: not rotated (or rotated by n).
Stuck? Open one hint at a time
Hint 1
Compare nums[mid] with nums[hi]. If nums[mid] > nums[hi], the drop (and so the minimum) is to the right of mid.
Show the solution with explanations
Approach 1Compare with the right end
The idea

The test “nums[i] ≤ nums[last]” gives false…false, true…true. The minimum is at the first true.

JavaScript
function findMin(nums) {  let lo = 0, hi = nums.length - 1;  while (lo < hi) {    const mid = lo + Math.floor((hi - lo) / 2);    if (nums[mid] > nums[hi]) lo = mid + 1;   // minimum is strictly right of mid    else hi = mid;                            // mid could be the minimum  }  return nums[lo];}console.log(findMin([3, 4, 5, 1, 2]));       // 1console.log(findMin([4, 5, 6, 7, 0, 1, 2])); // 0console.log(findMin([11, 13, 15, 17]));      // 11
How it works
Dry run · [4, 5, 6, 7, 0, 1, 2]
lohimidnums[mid] > nums[hi]?next
063 (7)7 > 2 yeslo = 4
465 (1)1 > 2 nohi = 5
454 (0)0 > 1 nohi = 4

lo = hi = 4: the minimum is 0.

Which approach should you use?

Comparing with nums[hi] instead of nums[lo] handles the not-rotated case without a special check. (LeetCode 153.)

Q7Find a peak elementMedium

A peak is a value strictly larger than its neighbours (the space outside the array counts as −∞). Neighbouring values are never equal. Return the index of any peak, in O(log n) time.

Example 1
Input: [1, 2, 3, 1]
Output: 2
Explanation: 3 is larger than both of its neighbours.
Example 2
Input: [1, 2, 1, 3, 5, 6, 4]
Output: 5
Explanation: 6 is a peak. The value 2 at index 1 is also a peak, so either answer is accepted.
Stuck? Open one hint at a time
Hint 1
The array is not sorted, but binary search still works. Treat the space outside the array as −∞ (smaller than any value).
Hint 2
If nums[mid] < nums[mid + 1], you are on a rising slope. A peak must exist to the right.
Show the solution with explanations
Approach 1Walk uphill with binary search
The idea

Go towards the larger neighbour. A rising slope must end in a peak.

JavaScript
function findPeakElement(nums) {  let lo = 0, hi = nums.length - 1;  while (lo < hi) {    const mid = lo + Math.floor((hi - lo) / 2);    if (nums[mid] < nums[mid + 1]) lo = mid + 1;   // rising: a peak is to the right    else hi = mid;                                 // falling: mid or something left is a peak  }  return lo;}console.log(findPeakElement([1, 2, 3, 1]));          // 2console.log(findPeakElement([1, 2, 1, 3, 5, 6, 4])); // 5
How it works

O(log n) time. Binary search does not need a sorted array. It only needs a rule that tells you which half certainly contains an answer.

Which approach should you use?

Interviewers like this question because it shows that binary search does not always need sorted data. (LeetCode 162.)

Make it stick

Make it stick — close this page, then
  1. Write the closed-range binary search from memory, and dry-run it with a missing target.
  2. Write lower bound in the half-open style and explain why hi = mid (not mid − 1).
  3. Explain how to count occurrences of x in a sorted array in O(log n).
  4. Explain why one half of a rotated sorted array is always sorted.

Revisit schedule: solve two of the questions again tomorrow, again in 3 days, and once more in a week — without looking at the answers. Each time it will feel easier. That is the memory forming.

What's next

Lesson 29 uses the “first true” idea on answers: the smallest speed, the smallest capacity, the largest minimum. Sometimes checking a guess is easy, but finding the answer directly is hard. Then binary search on the answer often solves the problem.

Practise on LeetCode

Real interview problems that use exactly what this lesson teaches. Solve them in JavaScript after the practice questions above.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series