An array of Cards holds references, all null until you fill them, and a nested loop fills all fifty-two. Then two ways to find a card in that array: looking at every element in turn, and — because the array is sorted and Card has compareTo — throwing away half the array with every comparison. Follows Think Java 2e, Chapter 12 (Arrays of Objects), Sections 12.6-12.9, pp. 208-213, cross-referenced against Java SE 21 API — java.util.Arrays (binarySearch).
Subject: Java · 65 slides · code lesson
Open the interactive version of this deck
Title
Think Java 2e · Chapter 12 · Arrays of Objects
Sections 12.6-12.9 · pp. 208-213
Objectives
This lesson follows Think Java 2e, Chapter 12 (Arrays of Objects), Sections 12.6-12.9, pp. 208-213. Everything on these slides can be checked against those pages.
1. Declare and populate an array of objects, and explain why the elements start as null.
2. Explain what causes a NullPointerException when using an array of objects.
3. Write a nested loop that enumerates every combination of two ranges, using a separate index variable.
4. Write a sequential search that returns an index, or -1 when the target is absent.
5. Write a binary search using low, high and mid, and explain why the array must be sorted.
6. Trace binary search by printing low and high, and count the comparisons it needs.
Warm-up
Three things from earlier lessons are about to be combined.
Discussion prompt
From Lesson 7a: what are the elements of new int[5] initialised to? From Lesson 9a: what is the value of a String variable that has been declared but not assigned? And from Lesson 12a: what does compareTo return?
Hint: Two zeros of different kinds, and a sign.
Answer:
new int[5] gives five zeros. An unassigned object variable is null — a special value meaning no object. And compareTo returns a negative number, zero or a positive number.
This lesson puts all three together: an array of objects starts full of nulls, has to be filled with real objects, and can then be searched using compareTo. The searching is the point — it is the first algorithmic choice in this book that changes how expensive an answer is, not just whether you get one.
Concept
Now that we have a Card class with an ordering, we can put cards in an array — and once they are in order, finding one becomes a question with two very different answers.
Figure (svg): A pipeline from declaring an array through populating it to two different search strategies
Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 12 (Arrays of Objects), Sections 12.6-12.9, pp. 208-213 — Sections 12.6 through 12.9 run from printed page 208 to 213.
Section
Section 12.6
Concept
Since a deck contains multiple cards, the natural choice is an array of Cards. The syntax is the same as for arrays of primitives — but what you get is different in one important way.
Card[] cards = new Card[52];| after this line | value |
|---|---|
| cards | a reference to a 52-element array |
| cards.length | 52 |
| cards[0] | null |
| number of Card objects created | 0 |
null — A special value that means no object — the initial value of every element of an array of objects.
The array is initially full of null references, because the elements are object variables and object variables start out as null. That is Lesson 9a's rule, applied fifty-two times at once.
Picture it
One array object exists. Fifty-two references exist inside it, and every one of them points at nothing.
Figure (svg): An array of six slots each holding null, with an arrow from the variable cards to the array
Before we can use the array we have to create cards and assign references to them. Compare this with new int[52], where the array is fifty-two integers — here the array is fifty-two empty slots.
Worked example
Fifty-two cards means every combination of four suits and thirteen ranks — which is exactly what a nested loop enumerates.
Card[] cards = new Card[52];
int index = 0;
for (int suit = 0; suit <= 3; suit++) {
for (int rank = 1; rank <= 13; rank++) {
cards[index] = new Card(rank, suit);
index++;
}
}| suit | rank | index | card stored |
|---|---|---|---|
| 0 | 1 | 0 | Ace of Clubs |
| 0 | 2 | 1 | 2 of Clubs |
| 0 | 13 | 12 | King of Clubs |
| 1 | 1 | 13 | Ace of Diamonds |
| 3 | 13 | 51 | King of Spades |
The outer loop runs over the four suits.
Why: suit goes 0 to 3, matching the encoding from Lesson 12a.
The inner loop runs over the thirteen ranks.
Why: rank goes 1 to 13, not 0 to 12, because Ace is 1.
Each iteration creates a card and stores it.
Why: cards[index] = new Card(rank, suit);
A separate variable tracks the position.
Why: The variable index keeps track of where in the deck the next card should go. It runs 0 to 51 while suit and rank cycle.
Verify: Print cards[0] and cards[51] and expect Ace of Clubs and King of Spades.
Why: Notice that index is neither suit nor rank — it is a third counter, incremented once per card. Trying to compute it as suit * 13 + rank would work too, but only if you remember to subtract 1 from rank, which is exactly the off-by-one the separate variable avoids.
Prediction
Nothing has been assigned yet.
Card[] cards = new Card[52];
System.out.println(cards[0]);| thing | exists? |
|---|---|
| the array object | yes |
| a Card at index 0 | no |
Predict first
What is printed?
Correct: null
Why: The array exists with 52 elements, and each element is an object variable whose initial value is null. Printing null is harmless — println displays the word null. It is calling a method on it, like cards[0].getRank(), that throws a NullPointerException.
Concept
Two loops in the same nest, with two different starting values. That asymmetry comes straight from the encoding.
| loop | range | because |
|---|---|---|
| suit | 0 to 3 | suits are encoded 0 to 3, so 4 values from 0 |
| rank | 1 to 13 | Ace is 1 and rank 0 is unused |
| index | 0 to 51 | an array index always starts at 0 |
Three counters, three different ranges, and only one of them is an array index. Writing rank <= 13 rather than rank < 13 is the visible consequence of the choice made in Lesson 12a to waste element zero — a choice that costs you exactly this one moment of care.
Trap
Every element is null until you assign something.
Card[] cards = new Card[52];
System.out.println(cards[0].getRank()); // NullPointerException| expression | value |
|---|---|
| cards | a valid array reference |
| cards[0] | null |
| cards[0].getRank() | NullPointerException |
If you try to access the rank of the zeroth card, you will get a NullPointerException. The array itself is fine; the element is not. This is Lesson 9a's null, met for the first time inside an array.
Create the objects, then use them.
Card[] cards = new Card[52];
// ... the nested loop fills every element ...
System.out.println(cards[0].getRank()); // 1| step | state of cards[0] |
|---|---|
| after new Card[52] | null |
| after the loop | a reference to the Ace of Clubs |
| then getRank() | 1 |
Declaring the array and creating the objects are two separate jobs, and forgetting the second is one of the most common causes of a NullPointerException. If you see one, look for an object you meant to create and did not.
Prediction
One increment per card created.
int index = 0;
for (int suit = 0; suit <= 3; suit++) {
for (int rank = 1; rank <= 13; rank++) {
cards[index] = new Card(rank, suit);
index++;
}
}| outer iterations | inner per outer | total |
|---|---|---|
| 4 | 13 | 52 |
Predict first
What is the value of index after the loops finish?
Correct: 52
Why: The body runs 4 × 13 = 52 times, and index is incremented after each, so it ends at 52 — one past the last index used. The final assignment was to cards[51], which is the familiar pattern: a counter that ends one beyond the last valid index.
Definition probe
Same syntax; different consequences.
Sort into buckets
Sort each statement.
Socratic
There are already two loop variables.
Discussion prompt
The loop could compute the position from suit and rank instead of keeping a third counter. Why does Think Java use a separate index — and what would the computed version look like?
Hint: Ranks start at 1.
Answer:
The computed version is cards[suit * 13 + rank - 1] — and that - 1 is the whole argument. Ranks run 1 to 13 while indexes run 0 to 12, so any formula has to correct for it.
The separate counter has no such correction to forget. It starts at 0, increments once per card, and is right by construction rather than by arithmetic.
A variable that counts what actually happened is safer than a formula that predicts it. The formula is not wrong — it is just one more place an off-by-one can hide, and this loop already has one asymmetric range to keep track of.
Section
Section 12.7
Concept
Searching for an element of an array is a common operation. The obvious way traverses the array, compares each element to the target, and returns the index where it was found.
public static int search(Card[] cards, Card target) {
for (int i = 0; i < cards.length; i++) {
if (cards[i].equals(target)) {
return i;
}
}
return -1;
}| i | cards[i] | equals target? |
|---|---|---|
| 0 | Ace of Clubs | no |
| 1 | 2 of Clubs | no |
| 2 | 3 of Clubs | yes — return 2 |
sequential search — A search that traverses a collection, comparing each element to the target, until it finds a match or runs out.
The return value is an index, not a card — the caller already has the card they were looking for; what they want to know is where it is. That is the same design as String.indexOf from Lesson 6b.
Notation
The return -1 sits after the loop, and where it sits is what makes the method correct.
Annotate
String.indexOf uses.== would ask the wrong question — Lesson 11b's point.The structure is worth naming because it recurs: a loop that returns on success, and a single return after the loop for the failure case. Any search you ever write has this shape.
Worked example
The array is a new deck, in order. Trace what the loop actually examines.
Card target = new Card(3, 0); // 3 of Clubs
int index = search(cards, target);| i | cards[i] | compared? | match? |
|---|---|---|---|
| 0 | Ace of Clubs | yes | no |
| 1 | 2 of Clubs | yes | no |
| 2 | 3 of Clubs | yes | yes |
| 3 onwards | the other 49 | no — already returned | — |
Three comparisons, then a return.
Why: The method leaves at i = 2 and never touches the rest.
Now search for the King of Spades instead.
Why: That is at index 51, so it takes fifty-two comparisons.
And a card that is not there at all.
Why: Also fifty-two — the loop has to finish before it can say no.
Note what the cost depends on.
Why: Where the target is, not what it is. An early card is fast and a late card is slow, purely by position.
Verify: Search for the Ace of Clubs (1 comparison) and the King of Spades (52 comparisons) and confirm both find the card.
Why: Fifty-two comparisons for the worst case, and fifty-two for a card that is absent. The method is correct and it does not use any of the structure the array has — which is what the next section exploits.
Prediction
A new deck, in order, and the King of Spades is last.
// cards[0] is the Ace of Clubs
// cards[51] is the King of Spades
search(cards, new Card(13, 3));| target position | elements examined |
|---|---|
| index 0 | 1 |
| index 51 | 52 |
Predict first
How many elements does the sequential search examine?
Correct: 52
Why: It checks every element from index 0 up to and including index 51, so all fifty-two. The cost depends on where the target sits, and a card at the end is the worst case — the same cost as a card that is not in the deck at all.
Concept
Its one virtue is that it makes no assumptions. That is also the reason it cannot be faster.
| needs | sequential search |
|---|---|
| the array to be sorted | no |
| the elements to be comparable | no — only equals |
| random access to elements | no — it goes in order |
| comparisons for 52 cards, worst case | 52 |
| comparisons for a million elements | a million |
Because it assumes nothing, it works on anything — an unsorted array, a linked list, a stream of input. The price is in the last row: the work grows in exact proportion to the size of the collection, which is Lesson 10b's argument about quadratic concatenation, one degree gentler.
Trap
== on objects asks whether they are the same object.
if (cards[i] == target) { // almost always false
return i;
}| cards[i] | target | == | equals |
|---|---|---|---|
| the 3 of Clubs in the deck | a new Card(3, 0) | false | true |
| the 3 of Clubs in the deck | the same object | true | true |
The caller built their target with new Card(3, 0), so it is a different object from the one in the array even though it represents the same card. The search would find nothing and return -1.
equals asks whether they represent the same value.
if (cards[i].equals(target)) {
return i;
}| question | operator |
|---|---|
| is this the same object? | == |
| do these represent the same card? | equals |
| which does a search want? | equals |
This is the identity-against-equivalence distinction from Lesson 11b, and it is exactly why Card defines an equals at all. The worst part of the == bug is that it compiles and runs — it just quietly never finds anything.
Fill the middle
Return the index on a match, and a sentinel otherwise.
Fill in the blanks
for (int i = 0; i < cards.length; i++) equals}(target)) -1
}
return ___;
Why: equals compares what the cards represent, while == would compare object identity and almost always be false. -1 is the conventional not-found value because it is not a valid index — and it must be returned after the loop, since only a finished loop proves the target is absent.
Prediction
One statement, two possible placements.
for (int i = 0; i < cards.length; i++) {
if (cards[i].equals(target)) {
return i;
}
// (A) here?
}
// (B) here?| placement | reports not-found after |
|---|---|
| (A) inside the loop | checking one element |
| (B) after the loop | checking every element |
Predict first
Which placement is correct?
Correct: (B) — only a finished loop proves the card is absent
Why: Returning -1 inside the loop would give up after the first element that did not match, reporting not-found for a card sitting at index 1. This is the same structure as array11 in Lesson 8b: a positive answer can come from one element, but a negative answer requires checking all of them.
Explain it to yourself
The caller already has a card equal to the one being sought.
Discussion prompt
search returns an int, not a Card. What would returning the Card be useless for, and what does the index let the caller do?
Hint: What can you do with a position that you cannot do with a value?
Answer:
Returning the Card would tell the caller nothing new — they constructed an equal card to search with, so they already have one.
The index is the useful part: with it you can remove the card, replace it, swap it with another, or report where in the deck it sits. All of those need a position.
It is the same design as String.indexOf in Lesson 6b, and for the same reason. **A search answers where, not what** — and -1 works as the not-found value precisely because no position is -1.
Section
Section 12.8
Concept
If we know the cards are in order, we can do better. The algorithm you use to look up a word in a dictionary is not to start at the beginning and read every word — you open it in the middle and decide which half to keep.
// the dictionary algorithm
// 1. start in the middle
// 2. if the word you want comes before, keep the left half
// 3. if it comes after, keep the right half
// 4. repeat on what is left| step | words left to consider |
|---|---|
| start | 100,000 |
| after 1 look | 50,000 |
| after 2 looks | 25,000 |
| after 17 looks | 1 |
binary search — A search that repeatedly halves the range under consideration by comparing the target with the middle element.
That process is a binary search, and the key requirement is in the first four words of this section: if we know the cards are in order. Sequential search needs no such promise, and this is what it buys.
Picture it
Each comparison eliminates everything on one side of the middle — not one element, but half of what remains.
Figure (svg): A row of array positions showing the range shrinking from the full array to a quarter to an eighth
Sequential search removes one element per comparison; binary search removes half the remaining ones. That difference is not a small optimisation — it changes the shape of the cost.
Worked example
Two variables mark the range still under consideration, and every iteration shrinks it.
public static int binarySearch(Card[] cards, Card target) {
int low = 0;
int high = cards.length - 1;
while (low <= high) {
int mid = (low + high) / 2; // step 1
int comp = cards[mid].compareTo(target);
if (comp == 0) { // step 2
return mid;
} else if (comp < 0) { // step 3
low = mid + 1;
} else { // step 4
high = mid - 1;
}
}
return -1;
}| variable | meaning |
|---|---|
| low | the first index still worth considering |
| high | the last index still worth considering |
| mid | the middle of that range, recomputed each pass |
| comp | the result of comparing the middle card with the target |
Step 1 — find the middle of the range.
Why: int mid = (low + high) / 2; — integer division, so it rounds down.
Step 2 — if the middle card is the target, you are done.
Why: comp == 0 means equivalent, so return mid.
Step 3 — if the middle card is lower than the target, the target is to the right.
Why: Discard everything up to and including mid: low = mid + 1;
Step 4 — otherwise the target is to the left.
Why: high = mid - 1; — and if low ever passes high, the range is empty and the card is not there.
Verify: Search a sorted deck for the 3 of Clubs and confirm it returns 2 after about six comparisons rather than three.
Why: Notice that the method uses only compareTo — never <, never == on the objects, never getRank. Everything it needs to know about the ordering comes from the one method Lesson 12a wrote, which is why the same algorithm works for any comparable type.
Prediction
A 52-card array, before any comparison.
int low = 0;
int high = cards.length - 1; // 51
int mid = (low + high) / 2;| low | high | low + high | divided by 2 |
|---|---|---|---|
| 0 | 51 | 51 | 25 — integer division rounds down |
Predict first
What is mid?
Correct: 25
Why: 51 / 2 is 25 in integer division, which discards the fraction — Lesson 2b's rule. The exact middle of an even-length range does not exist, and rounding down is fine: either half is a legitimate choice as long as the loop keeps shrinking the range.
Concept
low = mid + 1 rather than low = mid is the difference between a search and an infinite loop.
low = mid + 1; // correct: mid has been checked, exclude it
high = mid - 1;
// low = mid; // BUG: if high = low + 1, mid = low, and
// // nothing changes - the loop never ends| low | high | mid | with low = mid + 1 | with low = mid |
|---|---|---|---|---|
| 4 | 5 | 4 | low becomes 5 — progress | low stays 4 — stuck |
| 5 | 5 | 5 | returns or empties the range | never reached |
The middle element has already been compared, so it can be excluded from the next range — and excluding it is what guarantees the range shrinks every time. A loop that might not shrink its range is Lesson 6a's infinite loop wearing a disguise.
Trap
The algorithm silently gives wrong answers.
Card[] cards = shuffledDeck(); // NOT sorted
binarySearch(cards, target); // returns -1, or the wrong index| array state | sequential search | binary search |
|---|---|---|
| sorted | correct | correct |
| shuffled | correct | wrong |
| does it crash? | — | no — that is the problem |
It does not throw anything. It compares with the middle element, concludes the target is in a half where it is not, and searches there — confidently, and wrongly.
Know the precondition, and state it.
/**
* Finds a card in a SORTED array.
* @param cards must be sorted by compareTo
* @return the index, or -1 if not present
*/
public static int binarySearch(Card[] cards, Card target) { ... }| algorithm | precondition | worst case for 52 cards |
|---|---|---|
| sequential search | none | 52 comparisons |
| binary search | the array must be sorted | about 6 comparisons |
The speed is bought with an assumption, and an assumption the compiler cannot check is one you have to document. That is the same reasoning as Lesson 12a's encoding: a contract nothing enforces has to be written down where the next reader will see it.
Prediction
The middle card compares as lower than the target.
int comp = cards[mid].compareTo(target);
// comp < 0 means cards[mid] is LOWER than target| comp | target is | adjust |
|---|---|---|
| < 0 | to the right of mid | ? |
| > 0 | to the left of mid | ? |
Predict first
When comp is negative, what happens?
Correct: low = mid + 1 — keep the right half
Why: A negative comp means the middle card is lower than the target, so anything at or below mid is too low and can be discarded. Moving low past mid keeps only the right half — and the + 1 excludes mid itself, which has already been compared.
Definition probe
One assumes nothing; one assumes sortedness.
Sort into buckets
Sort each situation by the search that fits.
Counterexample
Six comparisons against fifty-two looks decisive.
Discussion prompt
Name a situation where sequential search beats binary search, and one where sorting first would not be worth it.
Hint: What if you only search once?
Answer:
If the target is at index 0, sequential search takes one comparison and binary search takes about six. Binary search wins the worst case, not every case.
If the array is unsorted and you will search it only once, sorting it costs far more than the single sequential search you were going to do anyway.
The general point: an algorithm's cost includes its preconditions. Binary search is fast given a sorted array; getting one is not free, and it only pays off when you search many times.
Section
Section 12.9
Concept
One of the most common ways to debug a program is by adding print statements. For binary search, the two numbers worth watching are the ones that define the range.
while (low <= high) {
System.out.println(low + ", " + high);
int mid = (low + high) / 2;
...
}| pass | low | high | mid | range size |
|---|---|---|---|---|
| 1 | 0 | 51 | 25 | 52 |
| 2 | 0 | 24 | 12 | 25 |
| 3 | 13 | 24 | 18 | 12 |
| 4 | 13 | 17 | 15 | 5 |
| 5 | 13 | 14 | 13 | 2 |
Notice how the range shrinks by roughly half on each pass. That single observation tells you the algorithm is working — and if the numbers ever stopped changing, you would have found your infinite loop.
Notation
Each line of output is one pass through the loop, and the pattern in the numbers is the algorithm made visible.
Annotate
Tracing by printing is not a beginner's crutch — it is how you check that an algorithm you believe in is doing what you believe. Lesson 4b made the same case for incremental development.
Worked example
Six comparisons for fifty-two cards is not a coincidence — it is the answer to how many times can you halve 52 before reaching 1?
// how many halvings to get from n down to 1?
// 52 -> 26 -> 13 -> 7 -> 4 -> 2 -> 1
// that is 6 steps
//
// log2(52) is about 5.7| array size | sequential, worst case | binary, worst case |
|---|---|---|
| 52 | 52 | 6 |
| 1,000 | 1,000 | 10 |
| 1,000,000 | 1,000,000 | 20 |
| 1,000,000,000 | 1,000,000,000 | 30 |
Count the halvings for 52.
Why: 52, 26, 13, 7, 4, 2, 1 — six steps, matching the six lines of trace output.
Recognise the function.
Why: The number of times you can halve n is the base-2 logarithm of n, and log₂ 52 is about 5.7.
Read the table downward.
Why: Every time the size multiplies by a thousand, binary search costs about ten more comparisons.
Read the last row.
Why: A billion elements, thirty comparisons. That is the difference an assumption about order buys you.
Verify: Run the traced binary search on all 52 cards and confirm none takes more than six passes.
Why: Sequential search grows in proportion to n; binary search grows in proportion to log n. For 52 cards the difference is convenient. For a billion it is the difference between an instant answer and no answer at all — and this is the first time in the book that choosing an algorithm has mattered more than choosing syntax.
Prediction
Count the halvings.
// 1000 -> 500 -> 250 -> 125 -> 63 -> 32
// -> 16 -> 8 -> 4 -> 2 -> 1| n | halvings |
|---|---|
| 52 | about 6 |
| 1000 | about 10 |
Predict first
Roughly how many comparisons does binary search need in the worst case?
Correct: about 10
Why: Halving 1000 repeatedly reaches 1 in about ten steps, because log₂ 1000 is close to 10. Sequential search would need up to a thousand — and the gap widens with every increase in size, which is why the logarithm matters more than the constant.
Concept
Every piece of Chapter 12 turns out to have been building toward this method.
| from | what it contributed |
|---|---|
| 12.1 encoding | integers, so cards can be ordered at all |
| 12.3 class variables | one shared decoding table for readable output |
| 12.4 compareTo | the only comparison binary search uses |
| 12.5 immutability | cards can be shared and rearranged safely |
| 12.6 arrays of cards | the collection to search |
Binary search calls compareTo and nothing else. It never touches rank, suit, or the encoding — which means the identical algorithm works for any type with a compareTo, and is exactly why the Java library ships one method that searches arrays of anything.
Trap
Print statements are for finding out, not for shipping.
while (low <= high) {
System.out.println(low + ", " + high); // still here in the finished program
...
}| problem | effect |
|---|---|
| output nobody asked for | clutters every run |
| a search inside a loop | thousands of lines of noise |
| slows the method down | printing is far slower than comparing |
The opposite mistake is just as common: deleting the trace before you understand the output, then adding it back three times.
Add it, read it, understand it, then remove it — deliberately.
// while debugging:
System.out.println(low + ", " + high);
// once you have confirmed the range halves each pass,
// delete the line - you now know what the loop does| stage | the trace |
|---|---|
| writing the method | add it |
| checking the range shrinks | read it |
| confident it is right | remove it |
| a bug appears later | add it back — it costs one line |
The line is cheap to add and cheap to delete, which is exactly why it is such a good debugging tool. Lesson 4b's incremental development is the same habit: check the small thing, then move on.
Prediction
The very first line printed, for a 52-card deck.
int low = 0;
int high = cards.length - 1;
while (low <= high) {
System.out.println(low + ", " + high);| variable | initial value |
|---|---|
| low | 0 |
| high | cards.length - 1 |
Predict first
What is the first line of output?
Correct: 0, 51
Why: high starts at cards.length - 1, which is 51 for a 52-element array — the last valid index, not the length. Using 52 would be an off-by-one that eventually indexes past the end of the array.
Fill the middle
The middle card is higher than the target.
Fill in the blanks
} else high} = mid - 1;
}
Why: A positive comp means the middle card is higher than the target, so the target must be to the left — and high moves down to just below mid. The - 1 excludes mid, which has already been compared, and excluding it is what guarantees the range shrinks and the loop terminates.
Explain it to yourself
All three change every pass.
Discussion prompt
The trace prints low and high. What do those two show that mid alone would not?
Hint: What are you trying to confirm?
Answer:
low and high define the range still under consideration, so the difference between them is the amount of work left. That is the quantity you want to watch halving.
mid on its own is just a position — it jumps about, and you cannot tell from it whether progress is being made.
Trace the thing whose behaviour you are checking. The claim is the range halves each pass, so the trace should print the range — which is a general habit worth more than this one example.
Section
Sections 12.6-12.9 together
Concept
Everything before this chapter was about making a program work. Binary search is the first place where two correct programs differ enormously in what they cost.
// both of these are CORRECT
search(cards, target); // up to 52 comparisons
binarySearch(cards, target); // about 6
// for a million elements:
// sequential: up to 1,000,000
// binary: about 20| sequential | binary | |
|---|---|---|
| correct? | yes | yes |
| needs a sorted array? | no | yes |
| cost for n elements | proportional to n | proportional to log n |
| cost for a billion | a billion | about 30 |
Correctness is not the only question. Lesson 10b hinted at this with StringBuilder, where concatenating in a loop was quadratic; here the point is explicit, and it is the door into everything that follows in a data structures course.
Picture it
The difference is not that binary search is faster by some factor. It is that the two grow differently.
Figure (svg): Two columns comparing how sequential and binary search costs grow as the collection size increases
Doubling the collection adds exactly one comparison to a binary search. That is what a logarithm means, and it is why sorted data is worth the trouble of keeping sorted.
Worked example
Both algorithms exist in java.util.Arrays, and knowing what they require is the point of having written them.
import java.util.Arrays;
Arrays.sort(cards); // needs compareTo
int i = Arrays.binarySearch(cards, target); // needs a SORTED array| method | requires | returns |
|---|---|---|
| Arrays.sort | elements with a compareTo | nothing — sorts in place |
| Arrays.binarySearch | the array already sorted | the index, or a negative number |
| what Card had to provide | compareTo | — |
Notice what both need from Card.
Why: A compareTo — the method Lesson 12a wrote. Neither knows anything about ranks or suits.
Notice what binarySearch assumes.
Why: The same precondition your own version had. The library cannot check it either.
Notice the return value.
Why: It returns the index when found, and a negative number when not — so the caller still tests the sign.
Notice why writing it yourself mattered.
Why: Using a method whose cost and preconditions you understand is different from using one you do not.
Verify: Sort an array of cards with Arrays.sort and confirm the order matches your compareTo — Clubs first, Aces low.
Why: The library's version is better tested and faster than yours. What it is not, is magic: it halves the range exactly as your version does, and it goes wrong on an unsorted array exactly as yours does.
Prediction
104 cards instead of 52.
// binary search on 52 cards: about 6 comparisons
// binary search on 104 cards: ?| n | halvings |
|---|---|
| 52 | 6 |
| 104 | ? |
Predict first
How many comparisons does the doubled array need?
Correct: About 7 — one more
Why: Doubling the size adds exactly one halving, because one extra comparison cuts the doubled array back down to the original size. That is what growing proportionally to log n means — and it is why binary search stays practical at sizes where sequential search is hopeless.
Concept
Card was designed to be used, and the next two chapters use it. Every decision from Lesson 12a is about to be cashed in.
| chapter | builds | relies on |
|---|---|---|
| 12 | the Card class | encoding, compareTo, immutability |
| 13 | a Deck class | arrays of cards, shuffling, sorting |
| 14 | a game of Crazy Eights | everything above |
Shuffling rearranges references, not cards — which is safe only because Card is immutable, and cheap only because moving a reference costs nothing. The design decisions in Lesson 12a were not abstract tidiness; Chapter 13 is where they pay.
Trap
Micro-optimising a sequential search.
// "let me make this faster"
for (int i = 0; i < cards.length; i++) {
if (cards[i].getRank() == target.getRank()
&& cards[i].getSuit() == target.getSuit()) {
return i;
}
}| change | effect on 52 cards | effect on a million |
|---|---|---|
| avoid a method call | saves microseconds | saves microseconds |
| still examines every element | 52 comparisons | 1,000,000 comparisons |
It also reaches past the class's own equals to compare its fields by hand, which is Lesson 11a's encapsulation given up for a saving that does not register.
Change the algorithm, not the line.
// sort once, then search many times
Arrays.sort(cards);
int i = Arrays.binarySearch(cards, target);| approach | comparisons for a million elements |
|---|---|
| a faster loop body | still a million |
| a different algorithm | about 20 |
A better algorithm beats a faster loop, always, once the collection is large enough. That is the single most useful thing to take from Chapter 12, and it is why the chapter spends four sections on search rather than one.
Definition probe
Preconditions are part of an algorithm's cost.
Sort into buckets
Sort each requirement.
Matching
Binary search, worst case.
Match the pairs
Why: Each thousandfold increase in size costs about ten more comparisons, because log₂ 1000 is about 10. The last row is the striking one: a billion elements, thirty comparisons — a sequential search of the same collection would take a billion.
Real world
The algorithm predates computers.
Discussion prompt
Think Java introduces binary search by describing how you look up a word in a dictionary. Where else do people do this without calling it an algorithm?
Hint: Guessing games, phone books, page numbers.
Answer:
Guess-my-number, where the answer is higher or lower: guessing the midpoint each time finds any number from 1 to 100 in seven guesses.
Finding a page in a book, looking up a name in an index, and — closer to programming — bisecting a version history to find the commit that broke something, which is exactly this algorithm applied to time.
All of them need the same precondition: the thing must be in order. A dictionary works because it is alphabetical; git bisect works because commits are chronological. That requirement is what the speed is bought with.
Comparison
Fill the blanks from what each algorithm assumes.
Comparison matrix
| sequential search | binary search | |
|---|---|---|
| needs the array sorted | no | yes |
| method it calls on the elements | equals | compareTo |
| comparisons for 52 cards, worst case | 52 | about 6 |
| comparisons for a million, worst case | 1,000,000 | about 20 |
| what happens on unsorted data | correct | silently wrong |
The last row is the one to remember. Binary search does not fail loudly on an unsorted array — it returns a wrong answer with complete confidence, which makes the precondition worth documenting.
Pattern
Two shapes worth recognising: the search that returns an index or -1, and the loop that halves a range.
// the search shape: return on success, -1 after the loop
for (int i = 0; i < a.length; i++) {
if (a[i].equals(target)) {
return i;
}
}
return -1;
// the halving shape: two bounds, and a middle that is excluded
int low = 0, high = a.length - 1;
while (low <= high) {
int mid = (low + high) / 2;
if (found) return mid;
else if (tooLow) low = mid + 1;
else high = mid - 1;
}
return -1;| the shape | the detail that makes it correct |
|---|---|
| search | return -1 AFTER the loop, not inside it |
| search | equals, not == |
| halving | low <= high, so a single-element range is still checked |
| halving | mid + 1 and mid - 1, so the range always shrinks |
| halving | the array must already be sorted |
Check
Work it out before you click.
Card[] cards = new Card[52];
System.out.println(cards[0].getRank());| thing | state |
|---|---|
| the array | created, 52 elements |
| cards[0] | null |
Check your understanding
What happens?
Answer: A
Why: new Card[52] creates the array but no Card objects, so every element is null. Printing null would be harmless, but calling a method on it is not — there is no object to call it on, so the program throws a NullPointerException at run time.
System.out.println(cards[0]) would print null. The method call is what throws.Check
Work it out before you click.
int low = 0;
int high = 51;
int mid = (low + high) / 2;
int comp = cards[mid].compareTo(target);
// comp is negative| mid | comp | meaning |
|---|---|---|
| 25 | negative | cards[25] is lower than target |
Check your understanding
What are low and high on the next pass?
Answer: A
Why: A negative comp means the middle card is lower than the target, so the target must be to the right and everything up to and including index 25 can be discarded — low = mid + 1 gives 26, and high is unchanged. The + 1 excludes mid itself, which guarantees the range shrinks every pass.
low = mid leaves mid in the range even though it has been compared, which can stop the range shrinking and hang the loop.Check
Work it out before you click.
// an array of 1,000,000 sorted elements
// worst case for each search:| algorithm | comparisons |
|---|---|
| sequential | ? |
| binary | ? |
Check your understanding
What are the two worst-case costs?
Answer: A
Why: Sequential search examines every element in the worst case, so a million. Binary search halves the range each time, and a million can be halved about twenty times before reaching one — because log₂ 1,000,000 is just under 20. That gap is the whole reason the chapter bothers with sortedness.
Real world
Binary search explains a design choice you meet everywhere data is stored.
Discussion prompt
Databases spend real effort keeping indexes in sorted order, even though it makes every insertion slower. Given what this lesson showed, why is that trade worth making?
Hint: How many times is a row written, and how many times is it read?
Answer:
Because the sorting is paid once per write and the saving is collected on every read — and most data is read far more often than it is written.
A table with a million rows and no index means a sequential scan: a million comparisons per query. With a sorted index it is about twenty. That is the same table from this lesson, with rows instead of cards.
It also explains why adding an index makes writes slower — the sorted order has to be maintained. The precondition is not free; it is prepaid, which is exactly the trade-off you were asked to weigh when deciding whether sorting first was worth it.
Commit first
Commit to an answer and to your confidence.
Predict first
What happens when you run binary search on an array that is not sorted?
Correct: It compiles, runs, and returns a wrong answer without any error
Why: Nothing in the code checks whether the array is sorted, and nothing could without examining every element — which would cost as much as the search it is trying to avoid. So the algorithm compares with the middle element, concludes the target lies in a half where it does not, and searches there. It returns -1 for a card that is present, or occasionally a wrong index. A silently wrong answer is worse than a crash, which is why the precondition belongs in the documentation.
Explain it
Two minutes, out loud.
Discussion prompt
A classmate says binary search is just a faster search. Explain why that undersells it, using the numbers for 52 and for a million — and say what it costs.
Hint: How does each cost grow?
Answer:
It is not faster by a fixed amount — the two grow differently. Sequential search costs one comparison per element, so a million elements means a million comparisons. Binary search halves the range each time, so a million costs about twenty.
Doubling the collection adds a million comparisons to one and exactly one comparison to the other. That is the difference, and it gets bigger as the data gets bigger.
What it costs is an assumption: the array must already be sorted, and nothing checks that. On unsorted data it gives a wrong answer without complaining. Naming the precondition is half the explanation — an algorithm's requirements are part of what it is.
Exit ticket
One question before you close the deck.
Predict first
Why does binary search need the array to be sorted?
Correct: Because comparing with the middle element only tells you which half to keep if the elements are in order
Why: The whole algorithm rests on one inference: if the target is greater than the middle element, it must be somewhere to the right. That inference is only valid when everything to the left of the middle is smaller and everything to the right is bigger — which is what sorted means. On a shuffled array the inference is simply false, and since nothing checks it, the method searches the wrong half and reports a confident wrong answer.
Connect it up
One page, from memory.
Draw it
Draw a 52-slot array with the first three slots labelled Ace of Clubs, 2 of Clubs, 3 of Clubs, and write beside it the nested loop that fills it — marking the three counters and their three different ranges. Then trace a binary search for the 3 of Clubs, writing the low and high values on each pass and drawing a line through the part of the array each comparison discards. Finish by writing the two worst-case costs for a million elements and one sentence on what the faster one assumes.
Recap
Four sections that take an array of objects and turn a linear search into a logarithmic one.
| if you remember one thing | it is this |
|---|---|
| about arrays of objects | the array and the objects are two separate creations |
| about searching | return -1 after the loop, never inside it |
| about cost | a better algorithm beats a faster loop |
equals and returns -1 after the loop.low and high, and uses only compareTo.Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.