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
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.
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)); // -1Traced: searching for 9
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");| nums | [1, 3, 5, 7, 9, 11, 13] |
| target | 9 |
| lo | 0 |
| hi | 6 |
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.
| Style | Start | Loop while | Moves | Use for |
|---|---|---|---|---|
| Closed [lo, hi] | hi = n − 1 | lo <= hi | lo = mid + 1, hi = mid − 1 | Finding an exact value |
| Half-open [lo, hi) | hi = n | lo < hi | lo = mid + 1, hi = mid | Finding a boundary (first index where…) |
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.
// 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| target | lower bound | upper bound | meaning |
|---|---|---|---|
| 2 | 1 | 4 | the 2s are at indices 1..3 |
| 3 | 4 | 4 | not present; insert at index 4 |
| 0 | 0 | 0 | smaller than everything |
| 9 | 6 | 6 | larger 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:
// 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 2This 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.
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| lo | hi | mid | sorted half | 0 inside it? | next |
|---|---|---|---|---|---|
| 0 | 6 | 3 (7) | left: 4..7 | no | lo = 4 |
| 4 | 6 | 5 (1) | left: 0..1 | yes | hi = 4 |
| 4 | 4 | 4 (0) | — | found | return 4 |
Practice questions
For each question, first write the yes/no test that changes from false to true. The code then follows from it.
Return the index of target in the sorted array, or −1. Use O(log n) time.
Hint 1
Show the solution with explanations
The template from the lesson.
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)); // -1O(log n) time, O(1) extra space.
Practise writing it until you can do it without thinking. (LeetCode 704.)
Return the index of target if found; otherwise the index where it would be inserted to keep the array sorted.
Hint 1
Show the solution with explanations
Half-open binary search for the first value ≥ target.
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)); // 4O(log n) time. Starting hi at nums.length (not length − 1) is what allows the answer “after everything”.
(LeetCode 35.)
Return the first and last index of target in a sorted array, or [−1, −1], in O(log n).
Hint 1
Show all 2 solutions with explanations
Find the first 8 with indexOf (a simple scan), then walk right to the last 8.
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 ]O(n) time in the worst case (when all values are equal). The problem asks for O(log n), so this is not enough.
Use lowerBound(target) for the first index and upperBound(target) − 1 for the last index.
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 ]O(log n) time. One helper function with a flag gives both bounds.
This is a very common medium question. It checks whether you can change the template to find boundaries. (LeetCode 34.)
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.
Hint 1
Show the solution with explanations
This is binary search with a yes/no test (a predicate), over versions 1…n.
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)); // 1702766719O(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.
This is the clearest example of “find the first true”. (LeetCode 278.)
A sorted array of distinct values was rotated at an unknown index. Return the index of target, or −1, in O(log n).
Hint 1
Show the solution with explanations
The method from the lesson.
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)); // -1O(log n) time. The <= in nums[lo] <= nums[mid] handles the case lo = mid (a left half with one item).
(LeetCode 33.)
Return the smallest value of a rotated sorted array of distinct values, in O(log n).
Hint 1
Show the solution with explanations
The test “nums[i] ≤ nums[last]” gives false…false, true…true. The minimum is at the first true.
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| lo | hi | mid | nums[mid] > nums[hi]? | next |
|---|---|---|---|---|
| 0 | 6 | 3 (7) | 7 > 2 yes | lo = 4 |
| 4 | 6 | 5 (1) | 1 > 2 no | hi = 5 |
| 4 | 5 | 4 (0) | 0 > 1 no | hi = 4 |
lo = hi = 4: the minimum is 0.
Comparing with nums[hi] instead of nums[lo] handles the not-rotated case without a special check. (LeetCode 153.)
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.
Hint 1
Hint 2
Show the solution with explanations
Go towards the larger neighbour. A rising slope must end in a peak.
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])); // 5O(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.
Interviewers like this question because it shows that binary search does not always need sorted data. (LeetCode 162.)
Make it stick
- Write the closed-range binary search from memory, and dry-run it with a missing target.
- Write lower bound in the half-open style and explain why
hi = mid(notmid − 1). - Explain how to count occurrences of x in a sorted array in O(log n).
- 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