Arrays of Cards: Sequential and Binary Search

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

What this lesson covers

The lesson, slide by slide

1. Arrays of Cards: Sequential and Binary Search

Title

Think Java 2e · Chapter 12 · Arrays of Objects

Sections 12.6-12.9 · pp. 208-213

2. What you will be able to do

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.

3. Retrieve before you read

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.

4. Fifty-two cards, and finding one of them

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.

5. An array of cards is an array of nulls

Section

Section 12.6

6. Declaring the array does not create any cards

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 linevalue
cardsa reference to a 52-element array
cards.length52
cards[0]null
number of Card objects created0

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.

7. The array is created; the objects are not

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.

8. Populating the array with a nested loop

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++;
    }
}
suitrankindexcard stored
010Ace of Clubs
0212 of Clubs
01312King of Clubs
1113Ace of Diamonds
31351King 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.

9. What is cards[0] right after the declaration?

Prediction

Nothing has been assigned yet.

Card[] cards = new Card[52];
System.out.println(cards[0]);
thingexists?
the array objectyes
a Card at index 0no

Predict first

What is printed?

  • null
  • Ace of Clubs
  • 0
  • a NullPointerException is thrown

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.

10. Why the inner loop starts at 1

Concept

Two loops in the same nest, with two different starting values. That asymmetry comes straight from the encoding.

looprangebecause
suit0 to 3suits are encoded 0 to 3, so 4 values from 0
rank1 to 13Ace is 1 and rank 0 is unused
index0 to 51an 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.

11. Using the array before filling it

Trap

The trap

Every element is null until you assign something.

Card[] cards = new Card[52];
System.out.println(cards[0].getRank());   // NullPointerException
expressionvalue
cardsa 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.

The fix

Create the objects, then use them.

Card[] cards = new Card[52];
// ... the nested loop fills every element ...
System.out.println(cards[0].getRank());   // 1
stepstate of cards[0]
after new Card[52]null
after the loopa 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.

12. What does index end at?

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 iterationsinner per outertotal
41352

Predict first

What is the value of index after the loops finish?

  • 52
  • 51
  • 4
  • 13

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.

13. Array of ints against array of Cards

Definition probe

Same syntax; different consequences.

Sort into buckets

Sort each statement.

new int[52]
the elements start as 0; the array is usable straight after new
new Card[52]
the elements start as null; you must create objects before using the elements
prim
An array of primitives holds the values themselves, initialised to zero, so it is immediately usable.
obj
An array of objects holds references, all null until assigned, so declaring the array creates no objects at all.

14. Why a separate index variable?

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.

15. Sequential search

Section

Section 12.7

16. Look at every element until you find it

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;
}
icards[i]equals target?
0Ace of Clubsno
12 of Clubsno
23 of Clubsyes — 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.

17. Two ways for the method to end

Notation

The return -1 sits after the loop, and where it sits is what makes the method correct.

Annotate

  • Returning from inside the loop stops it early. As soon as a match is found there is no reason to look at the rest.
  • Reaching the line after the loop means every element was checked and none matched. That is the only way to get there.
  • -1 is the conventional not-found value because it is not a valid index — the same convention String.indexOf uses.
  • Returning -1 inside the loop would be a bug: it would report failure after checking only the first element. This is Lesson 8b's array11 shape exactly.
  • equals, not ==. Two Card objects representing the same card are different objects, so == 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.

18. Searching for the 3 of Clubs

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);
icards[i]compared?match?
0Ace of Clubsyesno
12 of Clubsyesno
23 of Clubsyesyes
3 onwardsthe other 49no — 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.

19. How many comparisons to find the King of Spades?

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 positionelements examined
index 01
index 5152

Predict first

How many elements does the sequential search examine?

  • 52
  • 51
  • 26
  • 6

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.

20. What sequential search does not need

Concept

Its one virtue is that it makes no assumptions. That is also the reason it cannot be faster.

needssequential search
the array to be sortedno
the elements to be comparableno — only equals
random access to elementsno — it goes in order
comparisons for 52 cards, worst case52
comparisons for a million elementsa 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.

21. Comparing cards with ==

Trap

The 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 decka new Card(3, 0)falsetrue
the 3 of Clubs in the deckthe same objecttruetrue

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.

The fix

equals asks whether they represent the same value.

if (cards[i].equals(target)) {
    return i;
}
questionoperator
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.

22. Finish the sequential search

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.

23. Where must return -1 go?

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?
placementreports not-found after
(A) inside the loopchecking one element
(B) after the loopchecking every element

Predict first

Which placement is correct?

  • (B) — only a finished loop proves the card is absent
  • (A) — it returns sooner
  • Either works
  • Neither; it should be inside the if

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.

24. Why return an index rather than the card?

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.

25. Binary search

Section

Section 12.8

26. If the cards are in order, you can throw half away

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
stepwords left to consider
start100,000
after 1 look50,000
after 2 looks25,000
after 17 looks1

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.

27. Halving a sorted array

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.

28. The binary search method

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;
}
variablemeaning
lowthe first index still worth considering
highthe last index still worth considering
midthe middle of that range, recomputed each pass
compthe 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.

29. What is mid on the first pass?

Prediction

A 52-card array, before any comparison.

int low = 0;
int high = cards.length - 1;    // 51
int mid = (low + high) / 2;
lowhighlow + highdivided by 2
0515125 — integer division rounds down

Predict first

What is mid?

  • 25
  • 26
  • 25.5
  • 51

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.

30. The `+ 1` and the `- 1` are not decoration

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
lowhighmidwith low = mid + 1with low = mid
454low becomes 5 — progresslow stays 4 — stuck
555returns or empties the rangenever 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.

31. Binary search on an unsorted array

Trap

The trap

The algorithm silently gives wrong answers.

Card[] cards = shuffledDeck();     // NOT sorted
binarySearch(cards, target);        // returns -1, or the wrong index
array statesequential searchbinary search
sortedcorrectcorrect
shuffledcorrectwrong
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.

The fix

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) { ... }
algorithmpreconditionworst case for 52 cards
sequential searchnone52 comparisons
binary searchthe array must be sortedabout 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.

32. Which way does the range move?

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
comptarget isadjust
< 0to the right of mid?
> 0to the left of mid?

Predict first

When comp is negative, what happens?

  • low = mid + 1 — keep the right half
  • high = mid - 1 — keep the left half
  • return mid
  • return -1

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.

33. Which algorithm for which situation?

Definition probe

One assumes nothing; one assumes sortedness.

Sort into buckets

Sort each situation by the search that fits.

sequential search
a shuffled hand of cards; checking whether a value appears in unsorted input
binary search
a new deck, in order; a sorted list of a million names
seq
Nothing is known about the order, so every element has to be examined — and binary search would give a confidently wrong answer.
bin
The collection is sorted, so each comparison can eliminate half of what remains. The bigger the collection, the larger the saving.

34. Is binary search always faster?

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.

35. Tracing the code

Section

Section 12.9

36. Print low and high to see what the loop is doing

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;
    ...
}
passlowhighmidrange size
10512552
20241225
313241812
41317155
51314132

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.

37. Reading the trace

Notation

Each line of output is one pass through the loop, and the pattern in the numbers is the algorithm made visible.

Annotate

  • The first line is always 0 and length - 1, the whole array.
  • Each line has roughly half the span of the one above. 52, 25, 12, 5, 2, 1 — that is the halving, printed.
  • When low and high are equal, one position remains. The next comparison either finds the card or empties the range.
  • If low ever exceeds high the loop ends and the method returns -1 — the range became empty without a match.
  • Six lines for fifty-two cards. Count them: that is the whole argument for binary search on one screen.

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.

38. Counting the comparisons

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 sizesequential, worst casebinary, worst case
52526
1,0001,00010
1,000,0001,000,00020
1,000,000,0001,000,000,00030

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.

39. How many passes for 1000 elements?

Prediction

Count the halvings.

// 1000 -> 500 -> 250 -> 125 -> 63 -> 32
//      -> 16 -> 8 -> 4 -> 2 -> 1
nhalvings
52about 6
1000about 10

Predict first

Roughly how many comparisons does binary search need in the worst case?

  • about 10
  • about 500
  • 1000
  • about 100

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.

40. What made binary search possible

Concept

Every piece of Chapter 12 turns out to have been building toward this method.

fromwhat it contributed
12.1 encodingintegers, so cards can be ordered at all
12.3 class variablesone shared decoding table for readable output
12.4 compareTothe only comparison binary search uses
12.5 immutabilitycards can be shared and rearranged safely
12.6 arrays of cardsthe 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.

41. Leaving the trace in, or taking it out too soon

Trap

The trap

Print statements are for finding out, not for shipping.

while (low <= high) {
    System.out.println(low + ", " + high);   // still here in the finished program
    ...
}
problemeffect
output nobody asked forclutters every run
a search inside a loopthousands of lines of noise
slows the method downprinting 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.

The fix

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
stagethe trace
writing the methodadd it
checking the range shrinksread it
confident it is rightremove it
a bug appears lateradd 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.

42. What does the trace show first?

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);
variableinitial value
low0
highcards.length - 1

Predict first

What is the first line of output?

  • 0, 51
  • 0, 52
  • 0, 25
  • 25, 51

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.

43. Complete the halving

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.

44. Why print low and high rather than mid?

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.

45. Why this chapter matters

Section

Sections 12.6-12.9 together

46. The first time the algorithm mattered

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
sequentialbinary
correct?yesyes
needs a sorted array?noyes
cost for n elementsproportional to nproportional to log n
cost for a billiona billionabout 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.

47. Growth, not speed

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.

48. What the library already provides

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
methodrequiresreturns
Arrays.sortelements with a compareTonothing — sorts in place
Arrays.binarySearchthe array already sortedthe index, or a negative number
what Card had to providecompareTo—

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.

49. Doubling the deck

Prediction

104 cards instead of 52.

// binary search on 52 cards: about 6 comparisons
// binary search on 104 cards: ?
nhalvings
526
104?

Predict first

How many comparisons does the doubled array need?

  • About 7 — one more
  • About 12 — twice as many
  • About 6 — the same
  • About 104

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.

50. The three-chapter arc

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.

chapterbuildsrelies on
12the Card classencoding, compareTo, immutability
13a Deck classarrays of cards, shuffling, sorting
14a game of Crazy Eightseverything 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.

51. Optimising the wrong thing

Trap

The 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;
    }
}
changeeffect on 52 cardseffect on a million
avoid a method callsaves microsecondssaves microseconds
still examines every element52 comparisons1,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.

The fix

Change the algorithm, not the line.

// sort once, then search many times
Arrays.sort(cards);
int i = Arrays.binarySearch(cards, target);
approachcomparisons for a million elements
a faster loop bodystill a million
a different algorithmabout 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.

52. What does each algorithm require?

Definition probe

Preconditions are part of an algorithm's cost.

Sort into buckets

Sort each requirement.

sequential search
an equals method; nothing about the order
binary search
a compareTo method; the array to be sorted
seq
It compares each element to the target for equality and makes no assumption about order — which is why it works on anything and cannot be faster.
bin
It needs an ordering to decide which half to keep, so it needs both compareTo and the guarantee that the array is already in that order.

53. Match the size to the comparisons

Matching

Binary search, worst case.

Match the pairs

  • a. 52
  • b. 1,000
  • c. 1,000,000
  • d. 1,000,000,000
  • r1. about 6
  • r2. about 10
  • r3. about 20
  • r4. about 30

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.

54. Where have you used a binary search yourself?

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.

55. The two searches side by side

Comparison

Fill the blanks from what each algorithm assumes.

Comparison matrix

sequential searchbinary search
needs the array sortednoyes
method it calls on the elementsequalscompareTo
comparisons for 52 cards, worst case52about 6
comparisons for a million, worst case1,000,000about 20
what happens on unsorted datacorrectsilently 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.

56. The pattern to carry away

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 shapethe detail that makes it correct
searchreturn -1 AFTER the loop, not inside it
searchequals, not ==
halvinglow <= high, so a single-element range is still checked
halvingmid + 1 and mid - 1, so the range always shrinks
halvingthe array must already be sorted

57. Check: arrays of objects

Check

Work it out before you click.

Card[] cards = new Card[52];
System.out.println(cards[0].getRank());
thingstate
the arraycreated, 52 elements
cards[0]null

Check your understanding

What happens?

  • A. A NullPointerException — cards[0] is null, so it has no getRank to call (correct)
  • B. It prints 0
  • C. It prints null
  • D. It does not compile

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.

Why B tempts people
That would be the behaviour for an array of ints, whose elements really are initialised to zero.
Why C tempts people
System.out.println(cards[0]) would print null. The method call is what throws.
Why D tempts people
It compiles perfectly — cards[0] has type Card, and getRank is a Card method. The problem only appears at run time.

58. Check: binary search

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
midcompmeaning
25negativecards[25] is lower than target

Check your understanding

What are low and high on the next pass?

  • A. low = 26, high = 51 (correct)
  • B. low = 0, high = 24
  • C. low = 25, high = 51
  • D. low = 0, high = 25

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.

Why B tempts people
That is the adjustment for a positive comp, where the target is to the left.
Why C tempts people
low = mid leaves mid in the range even though it has been compared, which can stop the range shrinking and hang the loop.
Why D tempts people
This keeps the left half and includes mid — wrong half, and wrong bound.

59. Check: cost

Check

Work it out before you click.

// an array of 1,000,000 sorted elements
// worst case for each search:
algorithmcomparisons
sequential?
binary?

Check your understanding

What are the two worst-case costs?

  • A. 1,000,000 and about 20 (correct)
  • B. 1,000,000 and 500,000
  • C. about 20 and about 20
  • D. 1,000 and 20

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.

Why B tempts people
Half a million is the average for sequential search on a present element, not the worst case — and binary search is far below either.
Why C tempts people
Sequential search has no way to skip elements; it has no ordering to exploit.
Why D tempts people
A thousand would be the sequential cost for a thousand elements, not a million.

60. Sorted data is worth keeping sorted

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.

61. How sure are you?

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?

  • It compiles, runs, and returns a wrong answer without any error
  • It throws an exception
  • It falls back to a sequential search
  • It does not compile

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.

62. Explain it to someone else

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.

63. Exit ticket

Exit ticket

One question before you close the deck.

Predict first

Why does binary search need the array to be sorted?

  • Because comparing with the middle element only tells you which half to keep if the elements are in order
  • Because compareTo does not work on unsorted arrays
  • Because the array indexes would otherwise be wrong
  • Because it needs to know the array's length

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.

64. Draw the whole lesson

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.

65. Recap

Recap

Four sections that take an array of objects and turn a linear search into a logarithmic one.

if you remember one thingit is this
about arrays of objectsthe array and the objects are two separate creations
about searchingreturn -1 after the loop, never inside it
about costa better algorithm beats a faster loop

Sources

  1. 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
  2. Java SE 21 API — java.util.Arrays (binarySearch)
  3. Think Java 2e — free online edition and source code

Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108