Concepts / Binary Search

Binary Search

A recursive binary search has two base cases: an empty range, which returns -1, and a value found at the middle position, which returns that index.

  • Programming

A Search That Might Find Nothing

Binary search takes a sorted array and a target value, and returns the position where the target sits, or -1 when the target is absent. The routine has to answer two different questions: where the target is, and whether it is there at all. The second question is easy to overlook, because a routine that only handles successful searches looks correct on most inputs. This article therefore follows the routine's variables step by step and keeps one question in view throughout: how does the routine decide where to look next, and how does it know when to stop?

What do you think happens?

Given the sorted array 3, 7, 11, 15, 19, 24, 30 at indices 0 through 6, what should a binary search for the value 10 return?

  • The index where 10 would be inserted

  • -1

  • The index of the nearest smaller value

  • It never returns, because 10 is not present

Reveal answer
Answer:

-1

The value 10 is not in the array. The search keeps narrowing until the range is empty, and an empty range means the target cannot be present, so the routine returns -1. The trace in a later section shows each step.

Two Boundaries Instead of a Whole Array

Binary search works on a range of positions rather than on the whole array. The routine keeps track of a lower boundary and an upper boundary, and those two values mark the part of the array that could still contain the target. Each step inspects the item in the middle of that range, uses one comparison to decide which half to keep, and moves one boundary so that the discarded half falls outside the range.

A recursive version needs the same idea expressed as parameters. The original question was find this value in this array. The generalized question is find this value between these two positions of this array, inclusive at both ends. The parameters loIndex and hiIndex carry that range. The value being sought is passed along unchanged. Whole-array search is then just the special case where loIndex is 0 and hiIndex is the array length minus one, so the first call always looks like this.

python
Notes on the code
  • The empty-range check comes first, because an empty range has no middle element to compute.
  • mid is computed only after the empty-range check has passed.
  • Each recursive call receives a strictly smaller range, so the recursion must reach a base case.

Tracing a Search for a Present Value

Take the sorted array 3, 7, 11, 15, 19, 24, 30 at indices 0 through 6, and search for 24. The first call receives the whole range, 0 through 6. The middle position is 3, and the value there is 15. Because 24 is larger than 15, the lower half is discarded, including index 3 itself. The recursive call receives the range 4 through 6. Nothing in the array has changed; only the boundaries have moved.

3index 07index 111index 215index 3, middle19index 4, loIndex24index 530index 6, hiIndex
After the first comparison with the middle element 15, which positions are still in play, and how do loIndex and hiIndex move?

The middle of the range 4 through 6 is index 5, and the value there is 24. That matches the target, so the search returns 5. The sequence of calls can be read as a stack of recursive invocations, each with its own range, and the answer flows back up from the deepest call.

search(0, 6)middle 3, value 15, 24 islargersearch(4, 6)middle 5, value 24, matchreturn 5index of 24recurse on upper halfbase case: found
What ranges does each call receive, and what does the search return when it finds 24?
Worked example

Searching for 24 in a Seven-Item Array

A = [3, 7, 11, 15, 19, 24, 30], target 24, starting range 0 through 6.

  1. 1
    Call 1: range 0 to 6

    Middle is index 3, value 15. The target 24 is larger, so the lower half is discarded and the next range is 4 to 6.

  2. 2
    Call 2: range 4 to 6

    Middle is index 5, value 24. The value equals the target, so this is the successful base case.

  3. 3
    Return

    The index 5 is returned to the caller and passed back up through the calls.

Result

The search returns 5 after two calls.

Tracing a Search for an Absent Value

Now search the same array for 10, which is not present. The search must rule out every candidate before it can conclude that the target is missing. Each row in the table below shows one call, the range it receives, the middle position it inspects, and the decision it makes.

CallloIndexhiIndexmidA[mid]Decision
10631510 is smaller, keep lower half
2021710 is larger, keep upper half
32221110 is smaller, keep lower half
421nonenoneloIndex 2 is greater than hiIndex 1, range is empty, return -1

Trace of a recursive binary search for the absent value 10

The fourth call never computes a middle position. Its range is empty, because loIndex has moved past hiIndex, and the empty-range base case returns -1 immediately. This is the reason the empty-range check must come before any computation that depends on the range having elements. An empty range has no middle element, so trying to compute one would be meaningless.

0 to 6middle 30 to 2middle 12 to 2middle 22 to 1empty, return -110 below 1510 above 710 below 11
How does the range change on each call, and at which state does the search stop?

Why Sorted Order Makes One Comparison Enough

Inspecting a single item can rule out a large block of positions, but only because the array is sorted. If the items increase from left to right, comparing the target with one item tells you which side of that item the target must lie on. Everything on the far side can be discarded in one move. Each comparison therefore eliminates about half of the remaining possible positions.

The before and after view above makes the point. One comparison with the middle element 15 removed the lower four positions, leaving three candidates. Without sorting, that same comparison would say nothing about the positions it did not touch, so there would be no safe way to discard them. The discard decisions are valid only when the array increases from left to right.

Counting Steps Against Linear Search

Each step of binary search leaves about half of the remaining locations, so the number of candidates falls quickly. From a range of 1000 locations, the search narrows to about 500, then about 250, then about 125, and so on, until one location remains after about 10 steps. The number of steps is approximately the base-2 logarithm of the number of items, written as log2 n.

Formula
Items nBinary search steps (about log2 n)Linear search steps (worst case, n)
1,000about 10up to 1,000
1,000,000about 20up to 1,000,000
1,000,000,000about 30up to 1,000,000,000

Approximate step counts for finding a value in a sorted array

A million items need only about 20 binary search steps. A tenfold or thousandfold increase in items adds only a few more steps, because the count grows with the logarithm of the size. Linear search, which may inspect every element, grows in step with the size of the array itself. The gap between the two methods therefore widens quickly as the array grows.

Mistakes Beginners Make

  • Common mistake

    Forgetting the empty-range case

    What it looks like

    A routine that computes the middle position before checking whether loIndex is greater than hiIndex, then tries to read A at that position.

    Why it fails

    When the range is empty there is no middle element, so the computed position refers to nothing meaningful and the routine can fail or return a wrong answer.

    Do this instead

    Check loIndex greater than hiIndex first and return -1 before any middle computation.

  • Common mistake

    Believing the recursion always ends on the target

    What it looks like

    Writing the search as a chain of halvings that assumes the target will always turn up.

    Why it fails

    A recursive search is not guaranteed to find the value. It may reach an empty range when the target is absent.

    Do this instead

    Write both outcomes as explicit cases: a match at the middle returns its index, and an empty range returns -1.

  • Common mistake

    Passing the whole array's bounds into every call

    What it looks like

    Recursing with the same loIndex and hiIndex each time, or always using 0 and the array length minus one.

    Why it fails

    The recursive call must search only the part still under consideration, so the boundaries must change on each call.

    Do this instead

    Move loIndex to mid plus one or hiIndex to mid minus one, so each call works on a strictly smaller range.

  • Common mistake

    Searching an unsorted array

    What it looks like

    Applying the middle comparison to the values 30, 3, 19, 7, 24, 11, 15.

    Why it fails

    The discard decisions depend on increasing order. Unsorted data can cause the target to be discarded without ever being checked.

    Do this instead

    Sort the array first, or use a method that does not rely on order.

Practice Your Trace

MEDIUM

Using the array 3, 7, 11, 15, 19, 24, 30 at indices 0 through 6, trace the recursive binary search for the value 30. For each call, write down loIndex, hiIndex, the middle index, and the value there. State what the top-level call returns.

Hints
  • Start with loIndex 0 and hiIndex 6, then compute the middle as the floor of the average of the two boundaries.
  • Expect the search to discard the lower half on each call until index 6 is reached.
  • Check your final answer against the base case that returns the index of the matching element.

Key Takeaways

  • A recursive binary search has two base cases: an empty range returns -1, and a value found at the middle position returns its index.
  • The parameters loIndex and hiIndex mark the part of the array still under consideration, and each recursive call works on a strictly smaller range.
  • Binary search depends on a sorted array, because one comparison with the middle element can safely discard half of the remaining positions.
  • The number of steps is approximately log2 n, so a million items need about 20 steps, while linear search may need up to a million.