Room 11 of 14
· about 30 minutes
Binary search
Linear search looks at every element. This room is the faster search, and
the precondition that buys the speed.
Tasks checked0 of 4
XP earned on this path0
Room complete
Notes
Binary search requires a sorted array
Seven numbers in order: {1, 3, 5, 7, 9, 11, 13}. Compare a target against the middle one, 7. If the target is larger, the three slots left of 7 are out in one comparison, unread. That move is the entire speedup, and it is legal only because the order makes it true.
Java ships this search as Arrays.binarySearch, in java.util.Arrays. Hand it a sorted int[] and a target and it returns the slot the target sits in, or a negative number when it is absent. Now take the order away.
final int[] scrambled = {9, 1, 13, 3, 11, 5, 7};
System.out.println(Arrays.binarySearch(scrambled, 9));
Predict 9 is in that array, sitting in slot 0. What does that print?
-8. Negative, so it is reporting that 9 is not there. The first comparison reads the middle slot, which holds 3, and rules out everything smaller, so the left half goes unread with 9 at the front of it. Ask that same array for 7 and it returns 6, which is where 7 sits. Two calls, one right and one not.
It prints -8, with a 9 in slot 0 the whole time. No exception, no warning, just a number that is not true. Java's own documentation does not call that wrong, it calls it undefined: "If it is not sorted, the results are undefined."
Try it Put those two lines in a main, with import java.util.Arrays; at the top, and run it.
Then add Arrays.sort(scrambled); above the search and run again. It prints 4. Sorting a final array is legal: final locks the variable, not the values. Nothing here is recorded and nothing is timed.
Sort once, search many times
Sorting is not free, so the two algorithms pay at different moments. The costs below are big-O notation: how the work grows as the array grows, with n the number of slots. O(n) means double the array, double the work. O(log n) means double the array, add one step. O(n log n) is what a sort costs, well above O(n).
- Linear search: no preparation,
O(n) on every search. - Binary search:
O(n log n) once to sort, then O(log n) on every search after.
One search, and the sort is wasted. A thousand searches over the same data, and it is repaid many times over. The question to settle first is not which runs faster, it is how many times the data gets asked about.
What breaks Wrong: "Binary search is a built-in method, so it must sort the array if needed."
It does not, and nothing inside it looks. Sorting costs O(n log n) and rewrites the caller's array, so a search that quietly sorted would throw away the speed it exists for and change data nobody handed over. Arrays.sort sorts, Arrays.binarySearch assumes it happened.
Sorted means non-decreasing
Sorted means one particular comparison, and it has to be the comparison the search makes. For int[] that is non-decreasing order, xs[0] <= xs[1] <= xs[2] and onward, equal values allowed, so {1, 1, 2, 2, 3} counts. Reverse those seven and the results go undefined again, because xs[mid] > target stops meaning the target is to the left.
The speed has a price: one sort buys O(log n) on every search after it, and skipping it leaves the same fast code returning a number with nothing behind it.
Half the search range each step
The array never changes during a binary search. What changes is a pair of indices marking the part still worth looking at: low at the bottom, high at the top, everything between them in play.
Each pass reads the middle slot of that range, then moves low or high past the half the comparison ruled out, until the target turns up or low passes high, which is what an empty range looks like.
Predict Take the seven sorted values {1, 3, 5, 7, 9, 11, 13}. After one comparison against the middle, how many slots are still in play?
Three. The middle slot was read, so it is settled either way, and the comparison ruled out one of the two three-slot halves around it. Seven becomes three, then one, then none.
A million slots become half a million, then a quarter million, and about twenty passes later one slot is left. Twenty, because two multiplied by itself twenty times is about a million. O(log n) is the name for that shape.
public static int binarySearch(final int[] xs, final int target) {
int low = 0;
int high = xs.length - 1;
while (low <= high) {
final int mid = low + (high - low) / 2;
if (xs[mid] == target) {
return mid;
} else if (xs[mid] < target) {
low = mid + 1; // target, if present, is in the right half
} else {
high = mid - 1; // target, if present, is in the left half
}
}
return -1;
}
The trace, one row per pass
The array does not move, so a trace records only low, high, mid, and what the comparison decided. Searching those seven values for 9:
pass 1: low=0 high=6 mid=3 xs[3]=7 < 9 → low = 4
pass 2: low=4 high=6 mid=5 xs[5]=11 > 9 → high = 4
pass 3: low=4 high=4 mid=4 xs[4]=9 == 9 → return 4
Three passes against five. A slot-by-slot walk reads 1, 3, 5, 7, 9 before it stops, and that gap is what grows.
Three places the loop goes wrong
Each of these compiles and runs.
What breaks Wrong: "low < high is right, because the top of the range is not in it."
Both ends are in it. When low and high land on the same slot, that slot is still unread, and low < high exits before reading it. On the seven values above it finds 3, 7, and 11, calls the other four absent, and none of them look wrong.
Predict Change low = mid + 1 to low = mid and it still compiles. Search the seven values for 13. What happens?
The program stops responding at low=5, high=6, where mid computes to 5 again, reads 11, and sets low back to 5. The + 1 excludes the slot just read, and that is what makes progress.
The third is mid = (low + high) / 2, the midpoint as arithmetic suggests writing it. Write low + (high - low) / 2: the subtraction is never negative, so the result cannot run off the top of an int.
Dig deeper Why the naive midpoint is a defect and not a style preference
Once both indices climb near Integer.MAX_VALUE, low + high wraps negative, mid comes out negative, and xs[mid] throws ArrayIndexOutOfBoundsException. Array sizes here never reach that, so the habit is worth forming now: the fix costs one parenthesis and the failure appears only on data too big to test by hand.
Rabbit hole The JDK carried this defect for nine years
Joshua Bloch found it in java.util.Arrays.binarySearch and wrote it up on the Google Research blog in June 2006, claiming nearly all binary searches and mergesorts were broken. OpenJDK now uses (low + high) >>> 1, an unsigned right shift, which is the same fix. Bloch's write-up, and CWE-190.
The loop ends when low > high, an empty range, so nothing could match and the method returns -1. The JDK's own version usually returns some other negative number, and what those mean comes next.
The range closes either way: low and high close in whether or not the target is there, which is why the loop cannot run more than about twenty times on a million slots.
Arrays.binarySearch: the JDK built-in
The hand-written loop returns -1 for every miss, which answers one question and throws away another. The array was sorted, so the search knows more than absent: it knows where the value would have gone.
final int[] xs = {1, 3, 5, 7, 9, 11, 13};
System.out.println(Arrays.binarySearch(xs, 9)); // prints 4
System.out.println(Arrays.binarySearch(xs, 8)); // prints -5
System.out.println(Arrays.binarySearch(xs, 0)); // prints -1
System.out.println(Arrays.binarySearch(xs, 14)); // prints -8
Zero or higher is an index and the value was found there. Negative means absent, and the number carries the insertion point, the slot the value would occupy to keep the array sorted, encoded as -(insertionPoint) - 1. Decode it with int ip = -result - 1;.
Predict Two of those calls printed 4 and -5. Decode the second one. What do the answers share, and what separates them?
-5 decodes to -(-5) - 1, which is 4. Both point at slot 4: one is where 9 was found, the other where 8 would go. Same slot, opposite meanings, and the sign is all that tells them apart.
What breaks Wrong: "Arrays.binarySearch returns -1 when the target is not found."
It returns a family of negative numbers, and -1 only means "would go in slot 0". Three of the four calls above miss and one prints -1. Write if (result == -1) and the other two fall through to the found branch, where the program uses -5 or -8 as an index. Test result >= 0 for found, result < 0 for absent.
This encoding is stranger than a flat -1, and it exists because some callers need the insertion point: anything maintaining a sorted array wants to know where a new value goes, and the search already worked it out.
A negative return is a location: the sign says whether the value is there, and the magnitude says where it is or where it would go.