Sorted Arrays
Binary search finds an item in a sorted array by inspecting the middle of a shrinking range, and each comparison eliminates about half of the remaining possible positions.
Finding an Item Without Reading Every Line
Imagine a sorted list of one million numbers and a single target value you need to locate. A routine that checks each item in turn might have to look at every one of them. Binary search takes a different approach. It works on a sorted array, inspects one item near the middle of the range that could still hold the target, and uses the result of that single comparison to throw away a large block of positions at once.
The routine you will trace in this article takes a sorted array and a target value. It returns the position where the target sits, or the value -1 when the target is absent. As you read, keep one question in mind: how does the routine decide where to look next, and how does it know when to stop?
What do you think happens?
The array below is sorted. You search for the value 20, which is not in it. Before reading on, what do you think a correct search should return, and what would tell the routine that it can stop?
Reveal answer
It should return -1. The routine stops when the range of possible positions becomes empty, which means the target cannot be in the array.
A search routine has to answer two questions: where the target is, and whether it is there at all. A routine that only handles successful searches can look correct on many inputs, so the failing case deserves equal attention.
Why Sorting Makes Discarding Safe
The key fact behind binary search 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, comparing the target with one item tells you which side of that item the target must lie on. If the middle value is larger than the target, then the target cannot be at the middle or anywhere to its right, because every item on that side is larger still. All of those positions can be discarded in one move.
Tracing a Successful Search
The source describes the routine in a particular way. Rather than repeatedly modifying the array, it 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, and the result of that single comparison leads to one of three outcomes: the middle item is the target, the target must be in the lower part of the range, or the target must be in the upper part.
The working example uses the sorted array 2, 5, 8, 12, 16, 23, 38, 56, 72, 91, which occupies indices 0 through 9. The target is 23. The lower boundary starts at index 0 and the upper boundary starts at index 9. The middle of the range is the index halfway between them, rounded down.
Searching for 23 in a sorted array of ten items
Find the position of 23 in the sorted array 2, 5, 8, 12, 16, 23, 38, 56, 72, 91.
- 1Pass 1: range 0 to 9
The middle index is 4, which holds 16. Since 23 is greater than 16, the target cannot be at index 4 or anywhere to its left. The lower boundary moves to 5.
- 2Pass 2: range 5 to 9
The middle index is 7, which holds 56. Since 23 is less than 56, the target cannot be at index 7 or to its right. The upper boundary moves to 6.
- 3Pass 3: range 5 to 6
The middle index is 5, which holds 23. The middle item is the target, so the search stops and reports position 5.
The target 23 is at index 5. Three comparisons were needed, and the range shrank from ten possible positions to one.
Tracing a Search That Fails
The source states the stopping rule plainly. The loop runs while the lower boundary has not passed the upper boundary. It ends in one of two ways: by finding the target at the middle, or by the range becoming empty. When the upper boundary falls below the lower boundary, the target is known not to be in the array, and the routine returns -1.
Searching for 20, which is absent
Use the same sorted array 2, 5, 8, 12, 16, 23, 38, 56, 72, 91 and search for 20.
- 1Pass 1: range 0 to 9
The middle index is 4, which holds 16. Since 20 is greater than 16, the lower boundary moves to 5.
- 2Pass 2: range 5 to 9
The middle index is 7, which holds 56. Since 20 is less than 56, the upper boundary moves to 6.
- 3Pass 3: range 5 to 6
The middle index is 5, which holds 23. Since 20 is less than 23, the upper boundary moves to 4.
- 4Check: lower 5, upper 4
The upper boundary is now below the lower boundary. The range has no positions left, so the target cannot be in the array.
The routine returns -1. The search concluded that 20 is absent after three comparisons, without ever needing to read the whole array.
A Python Version of the Routine
The code below follows the description above: two boundary variables, a loop that runs while the range is non-empty, a single comparison per pass, and a return of -1 when the range becomes empty. It is an illustration written for this article, not a copy of the source's implementation.
def binary_search(items, target):
low = 0
high = len(items) - 1
while low <= high:
mid = (low + high) // 2
if items[mid] == target:
return mid
elif items[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1
numbers = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
print(binary_search(numbers, 23))
print(binary_search(numbers, 20))- 1Line 2 and 3 set the boundaries to the first and last valid indices.
- 2Line 4 keeps the loop running while low has not passed high.
- 3Line 5 finds the middle index of the current range.
- 4Lines 6 and 7 stop the search when the middle item equals the target.
- 5Lines 8 and 10 discard the half of the range that cannot contain the target.
- 6Line 11 returns -1 when the loop ends with an empty range.
- low and high are the two boundary variables that mark the range of possible positions
- The while condition keeps the loop running only while the range is not empty
- The middle index is computed from both boundaries at every pass
- Returning mid ends the search when the middle item is the target
- Moving low or high past the middle is the discard step that removes half the range
- Returning -1 after the loop reports that the target is absent
5
-1Comparing Steps as Arrays Grow
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. Looking through every element in turn would take far longer on the same array.
Because the number of steps is approximately the base-2 logarithm of the number of items, a million items need only about 20 steps. The logarithm grows very slowly. A tenfold or even a thousandfold increase in items adds only a few more steps. Linear search, which may inspect every element, grows in step with the size of the array itself. In the worst case it needs one comparison per item, so a million items can require up to a million comparisons.
Mistakes When Tracing the Search Range
Common mistake Applying binary search to an unsorted array
What it looks likeSearching the list 91, 2, 56, 8 for 8 and discarding the right half after comparing with the middle value.
Why it failsThe discard decisions are only valid when the array increases from left to right. An unsorted middle value says nothing reliable about the positions beside it.
Do this insteadConfirm the array is sorted before using binary search, or use linear search when it is not.
Common mistake Forgetting the empty-range case
What it looks likeA routine that loops only while low is less than high and returns nothing afterward.
Why it failsA search for an absent value must end somewhere. Without the empty-range check, the routine has no defined answer for failing searches, which is the case that is easiest to overlook.
Do this insteadLoop while low is not past high, and return -1 after the loop ends.
Common mistake Discarding the middle item along with the wrong half
What it looks likeAfter comparing 23 with the middle value 16, setting low to the middle index instead of one past it.
Why it failsThe middle item has already been checked and is not the target, so keeping it in the range wastes a comparison and can cause the range to stop shrinking.
Do this insteadMove low to middle plus one, or high to middle minus one, so the checked item leaves the range.
Common mistake Treating the number of steps as proportional to the array size instead of its logarithm
Practice: Trace a Search by Hand
Use the sorted array 2, 5, 8, 12, 16, 23, 38, 56, 72, 91, which occupies indices 0 through 9. Trace binary search for the value 72. For each pass, write the lower boundary, the upper boundary, the middle index, the value at the middle, and your decision. Report the final result.
Hints
- Start with low at 0 and high at 9, then compute the middle index as the midpoint rounded down.
- Compare the target with the middle value. If the target is larger, move low past the middle. If it is smaller, move high before the middle.
- The first middle index is 4, which holds 16. Decide which side of index 4 can contain 72.
Without tracing every pass, explain in two or three sentences why binary search on this array cannot need more than about four comparisons to answer for any target, and why the answer is still correct when the target is absent.
Hints
- Think about how the range of possible positions shrinks after each comparison.
- Consider what the routine does when the upper boundary falls below the lower boundary.
Key Takeaways
- Binary search works on a sorted array by inspecting the middle of a shrinking range of possible positions, and each comparison discards about half of the remaining positions.
- Sorting is what makes the discard safe: when the array increases from left to right, one comparison with the middle item shows which side the target must lie on.
- The loop runs while the lower boundary has not passed the upper boundary, and it ends either by finding the target or by the range becoming empty, in which case the routine returns -1.
- The number of steps is approximately the base-2 logarithm of the number of items, so a million items need only about 20 steps, while linear search may need up to a million comparisons.