Concepts / Binary Search on Sorted Arrays

Binary Search on Sorted Arrays

A binary sort tree is a binary tree in which every node is at least as large as everything in its left subtree and no larger than everything in its right subtree.

  • Programming

Why Sorted Storage Needs a Smarter Lookup

Suppose you must keep a list of strings in alphabetical order, and you repeatedly look items up and add new ones. A linked list is a natural first choice, but it has a cost. To add an item in the right place, you must walk through the list until you find that place, and on average that means examining about half of the items. Looking up an item costs roughly the same amount of work.

A sorted array improves the lookup side. Because the items sit in a known order, a binary search can locate an item quickly. The trouble then moves to insertion: making room for a new item in the middle of an array means shifting, on average, half of the existing items over by one position. This article is about the first half of that story, the fast lookup that sorted order makes possible, and then about a tree-shaped arrangement that also makes insertion efficient.

The routine you will meet below takes a sorted array and a target value. It returns the position in the array where the target sits, or the value -1 when the target is absent. Keep one question in mind as you read: how does the routine decide where to look next, and how does it know when to stop?

Narrowing Possible Positions Step by Step

A search 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 would look correct on most inputs. A correct routine must give a definite answer in both cases.

The key fact is that 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, then 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.

Rather than repeatedly modifying the array, the routine keeps track of the range of locations that could still contain the target. Two boundary variables mark that range, a lower boundary and an upper boundary. At each step, the routine looks at the item in the middle of the range. The result of that single comparison leads to one of three outcomes: the middle item is the target, the target lies in the upper half of the range, or the target lies in the lower half of the range.

index 02index 15index 28index 312index 416 (middle)index 523index 638index 756index 872index 991
After comparing the target 23 with the middle item 16 at index 4, which indices are still possible? Indices 0 through 4 are discarded, and indices 5 through 9 remain.

The loop runs while the lower boundary has not passed the upper boundary. Each pass inspects the middle, compares, and discards one half. The loop ends in one of two ways: either the target is found at the middle, or the range becomes empty. The source's comment on that final condition puts it plainly: when the upper boundary falls below the lower boundary, the target is known not to be in the array.

Key point

Each comparison in binary search leaves about half of the remaining possible positions. The search never needs to examine every element, because the sorted order lets one comparison rule out a whole block of positions at once.

Tracing a Successful Search

Worked example

Finding 23 in a sorted array of ten items

Search the sorted array [2, 5, 8, 12, 16, 23, 38, 56, 72, 91] for the target 23. The positions are numbered 0 to 9.

  1. 1
    Set the boundaries

    Start with low = 0 and high = 9, so every position from 0 to 9 is still possible.

  2. 2
    First middle item

    Compute mid = (0 + 9) // 2 = 4. The item at index 4 is 16. Since 23 is greater than 16, and the array increases from left to right, 23 can only be at index 5 or later. Set low = 5. Indices 0 to 4 are discarded.

  3. 3
    Second middle item

    Now the range is 5 to 9, so compute mid = (5 + 9) // 2 = 7. The item at index 7 is 56. Since 23 is smaller than 56, it can only be at index 6 or earlier within the range. Set high = 6. Indices 7 to 9 are discarded.

  4. 4
    Third middle item

    The range is now 5 to 6, so compute mid = (5 + 6) // 2 = 5. The item at index 5 is 23. This matches the target, so the search stops.

Result

23 is found at index 5 after three comparisons. A ten-item array needed only three inspections.

Code That Returns -1 When Nothing Matches

The following Python function is an illustrative version of the routine described in the source. It keeps a lower and an upper boundary, looks at the middle of the range, and returns -1 when the range becomes empty.

python
Output (expected)
5
Notes on the code
  • low and high mark the range of positions that could still hold the target
  • mid is the middle position of the current range
  • Moving high below mid discards the upper half; moving low above mid discards the lower half
  • The final return -1 is the answer when the range has become empty

Predicting a Failed Search

What do you think happens?

Using the same array [2, 5, 8, 12, 16, 23, 38, 56, 72, 91], what does the routine return when the target is 40?

  • -1

  • 6

  • 7

  • 38

Reveal answer
Answer:

-1

The range shrinks from 0 to 9, then to 5 to 9, then to 5 to 6, then to 6 to 6 after comparing 23 and then 38. After comparing 38, 40 is larger, so low moves to 7. Now low is 7 and high is 6, so the range is empty and the routine returns -1. The target is absent.

Binary Sort Trees as an Ordered Structure

When an ordered collection is arranged as a binary tree with a particular ordering rule, both searching and insertion can be efficient. A binary tree built this way is called a binary sort tree, often abbreviated BST. The ideas below explain the arrangement, the guarantees it provides, and the conditions under which those guarantees turn into speed.

Counting Steps: Binary Versus Linear

The source makes the efficiency argument concrete. Each step leaves about half of the remaining locations, so the number of locations left falls quickly. From a range of 1000 locations, the search narrows to about 500, then about 250, then about 125, and so on, until only one location remains after about 10 steps. The number of steps is approximately the base-2 logarithm of the number of items, so a million items need only about 20 steps.

Because the step count grows with the logarithm of the array size, a tenfold or even a thousandfold increase in items adds only a few more steps. Doubling the array adds roughly one more step, because the base-2 logarithm increases by one each time the size doubles. Linear search, which may inspect every element, grows in step with the size of the array itself.

Array sizeBinary search steps (approximate)Linear search worst case
1,000About 10Up to 1,000 inspections
2,000About 11Up to 2,000 inspections
1,000,000About 20Up to 1,000,000 inspections

Binary search figures follow the approximate base-2 logarithm of the size. Linear search may need to look at every element.

Mistakes Beginners Make

  • Common mistake

    Running binary search on an array that is not sorted

    What it looks like

    Searching [56, 2, 91, 8, 23] for 23 by comparing with the middle item and discarding a half.

    Why it fails

    The discard decision is valid only when the array increases from left to right. Comparing with one item says nothing reliable about the far side.

    Do this instead

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

  • Common mistake

    Forgetting the absent case

    What it looks like

    A loop that returns only when items[mid] equals the target and then ends without a final return.

    Why it fails

    A failed search needs a definite answer. The routine returns -1 when the range becomes empty.

    Do this instead

    After the loop, when low has passed high, return -1.

  • Common mistake

    Checking only the immediate children of each node

    What it looks like

    A tree with root 50, left child 30, and 60 as the right child of 30.

    Why it fails

    Node 30 is smaller than 50 and 60 is larger than 30, but 60 sits in the left subtree of 50 and is larger than 50, which breaks the rule.

    Do this instead

    Compare each node with everything in its left and right subtrees.

  • Common mistake

    Assuming any insertion order keeps the tree fast

Practice: Trace the Search

MEDIUM

Using the sorted array [2, 5, 8, 12, 16, 23, 38, 56, 72, 91], trace binary search for the target 40. List each mid index and the item found there, state which side of the middle item 40 lies on, and state the final result.

Hints
  • Start with low = 0 and high = 9.
  • After each comparison, move either low or high so that the discarded half is no longer in the range.
  • The search can end with a range that has become empty, which means 40 is absent.

Key Takeaways

  • Binary search finds an item in a sorted array by inspecting the middle of a shrinking range. Each comparison discards about half of the remaining possible positions.
  • Sorted order is what makes discarding valid: if the array increases from left to right, one comparison tells you which side of the middle the target must lie on.
  • A binary sort tree applies the same elimination idea to a tree. Every node is at least as large as its left subtree and no larger than its right subtree, and an inorder traversal therefore outputs the items in increasing order.
  • Search and insertion in a binary sort tree are efficient only when the tree stays close to balanced, and insertion order decides whether that happens.
  • Binary search needs about log base 2 of n steps, so a million items need only about 20, while linear search may need to inspect every element.