All topics
29

Finding one item in a sorted list by halving

Tier 1Free

Where it runs

1. The problem

Prefix sums relied on the data standing in a row, so you could add up any stretch. Sorted data has another useful property: order. When a list is in order, you don't need to look at everything to find one item. One check removes half of the list.

You have a million log lines, stored in order of time. Someone asks for the entry at 14:32:07. You could read from the top, line by line, until you reach it. On a million lines that is up to a million reads for one search, and searches happen all the time.

When the data is already sorted, any item out of a million is found in about 20 checks instead of a million.

2. In plain words

Take the sorted test list [1, 3, 5, 7, 9, 11] and look for 7. Look at a number in the middle: 5. It is smaller than 7. The list is sorted, so 1 and 3, which stand to the left of 5, are smaller still. One look removed three numbers. Only 7, 9 and 11 are left. Look at their middle: 9. It is bigger than 7, so 9 and 11 are out. Only 7 is left, and it is the one. Three looks instead of six.

The gain grows with the list. Each look removes about half of what is left. A million sorted log lines shrink to 500,000, then 250,000, then 125,000, and so on. After 19 looks one line is left, and the 20th look checks it. This works only because the list is in order. In an unordered list, a 5 that is smaller than 7 would tell you nothing about its neighbours.

3. The idea

Keep two markers, lo and hi. They mark the part of the array where the target may still be. At the start that is the whole array. Look at the element in the middle between them.

There are three outcomes. If the middle element equals the target, the search is over. If it is smaller than the target, the target can only be to its right, so move lo past the middle. If it is bigger, the target is to its left, so move hi before the middle. Each check removes half of what was left.

One rule protects you from an endless loop: set lo to mid + 1 and hi to mid - 1, and never set them to mid itself. If a marker lands on mid again, the range stops shrinking, and the loop runs forever. Stop when lo becomes bigger than hi. That means the target is not in the array, so return -1.

1 3 5 7 9 11 mid=5 < 7 drop left half 9 > 7: move hi left 7 found at index 3

Trace it on [1, 3, 5, 7, 9, 11], looking for 7.

  • Start: lo = 0, hi = 5. The middle is index 2, which holds 5. That is smaller than 7, so lo = 3.
  • Now lo = 3, hi = 5. The middle is index 4, which holds 9. That is bigger than 7, so hi = 3.
  • Now lo = 3, hi = 3. The middle is index 3, which holds 7. A match.

Found at index 3: three checks instead of six.

4. What it costs

Time is O(log n): each step halves what is left. A million items need about 20 steps, and a billion about 30. That is the almost flat curve from lesson 09: it barely rises as the data grows.

Memory is O(1): three index variables and nothing that grows with the array. There is one condition: the input must already be sorted. If it is not, sorting costs O(n log n). That pays off only when there will be many searches.

5. Where this shows up

Finding a record in sorted logs and time-series stores. A lookup in a B-tree (the structure behind database indexes) is binary search adapted to work with a disk. Finding a version in a sorted list of releases. The lower_bound and upper_bound functions in standard libraries. The git bisect command finds the commit where a bug appeared: it halves the range of commits every time. It is the same idea, applied to a history of versions in place of an array.

Find 13 in a sorted array by halving the range
binarySearch([1, 3, 5, 7, 9, 11, 13, 15], 13)
arr (sorted)
1
3
5
7
9
11
13
15

lo

hi
target = 13lo = 0hi = 7mid = -return = ?

Find 13 in a sorted array. lo and hi mark the range where it can still be: at first, the whole array.

Speed race
Binary search against a linear scan over the same 48 sorted numbers

Bars: the sorted numbers. Binary search halves the range at every comparison; the scan reads them one by one.

One operation is one comparison with the target. Every frame, each lane does the same number of operations, so the slow one falls behind.

Linear scan O(n)
0 operations
Binary search O(log n)
0 operations

 

And with more numbers

Operations for n numbers, and roughly how long they take at 100 million simple operations a second. An estimate from the same count, not a measurement.

n = 1,000
Linear scan900 · under 1 ms
Binary search10 · under 1 ms
n = 100,000
Linear scan90K · under 1 ms
Binary search16.6 · under 1 ms
n = 10,000,000
Linear scan9M · 90 ms
Binary search23.3 · under 1 ms

Recognise it

Binary search (array)

Say these words and it is this pattern

  • sorted array
  • find a target / first or last occurrence
  • n up to 1e6+ and O(log n) expected
  • find the boundary where a condition flips
  • search a rotated-but-mostly-sorted array (with a twist)

Looks similar, isn't

n up to 1e9 is fine, because log2 of 1e9 is about 30. Time is O(log n), space is O(1).

On LeetCode

Problems that look up a value in sorted data in O(log n), including a hidden number you can only ask about and a grid sorted row after row. Halve the range with every check.

Your task (Tier 1)

Write the function binarySearch(nums, target).

The list is sorted from low to high. Look at the number in the middle, then throw away the half where target can't be. Repeat until you find it or run out of numbers. Return the position of target, or -1 if it is not there.

  • Input: nums, a list of numbers sorted from low to high, and target, the number you look for. Example: [1, 3, 5, 7, 9, 11] with target 7.
  • Return: the position of target, or -1 if it is not there.

Steps:

  1. Set lo = 0 (the first position) and hi to the last position.
  2. While lo <= hi, take the middle position mid. If nums[mid] equals target, return mid. If nums[mid] is smaller than target, search the right half (lo = mid + 1). If it is bigger, search the left half (hi = mid - 1).
  3. If the range becomes empty with no match, return -1.

Done when all tests pass:

  • [1, 3, 5, 7, 9, 11], target 7 -> 3
  • [1, 2, 3], target 1 -> 0 (the first number)
  • [1, 2, 3], target 3 -> 2 (the last number)
  • [1, 2, 3], target 4 -> -1 (bigger than every number)
  • [2, 4], target 1 -> -1 (smaller than every number)
  • [], target 5 -> -1 (empty list, must not crash)
  • [5], target 5 -> 0 (one number)

Practice

binarySearch

binarySearch(nums, target) -> index (0-based) or -1

export function binarySearch(nums, target) {
  // Your code here. The task is described in the box above the editor.
}

Running as a guest

The tests run right here, no account needed. Your code is kept in this browser only. A free account keeps it on every device, marks the step done and shows where you are in the course.

Hint ladder

Stuck? Reveal one rung at a time, but try it yourself first.

    We use necessary cookies to run the site (sign-in and language). If you accept, we also load Google Analytics to see which pages are used, and Google reCAPTCHA to keep spam off the contact and bug-report forms. Privacy policy