A Deck class whose instance variable is an array of Cards, pseudocode as a way of discovering which helper methods you need, shuffling by repeated swaps, selection sort and why it is quadratic, and merge sort — which needs subdecks and a merge before it can be written at all. Follows Think Java 2e, Chapter 13 (Objects of Arrays), Sections 13.1-13.6, pp. 217-224, cross-referenced against Java SE 21 API — java.util.Collections (shuffle, sort).
Subject: Java · 65 slides · code lesson
Open the interactive version of this deck
Title
Think Java 2e · Chapter 13 · Objects of Arrays
Sections 13.1-13.6 · pp. 217-224
Objectives
This lesson follows Think Java 2e, Chapter 13 (Objects of Arrays), Sections 13.1-13.6, pp. 217-224. Everything on these slides can be checked against those pages.
1. Write a class whose instance variable is an array, with more than one constructor.
2. Convert a static method into an instance method and explain why the code gets shorter.
3. Use pseudocode to design an algorithm and discover the helper methods it needs.
4. Explain what a helper method is and why helper methods are usually private.
5. Describe selection sort and explain why its cost is proportional to n².
6. Explain why merge sort's n log₂ n cost matters, and write subdeck without an off-by-one error.
Warm-up
Chapter 13 picks up exactly where Chapter 12 stopped.
Discussion prompt
From Lesson 12b: what does new Card[52] create, and what does it not create? And from Lesson 12a: why is it safe for two different collections to hold references to the same Card object?
Hint: One array, no cards. And one keyword per instance variable.
Answer:
new Card[52] creates an array of fifty-two null references and no Card objects at all. And sharing cards is safe because Card is immutable — with final instance variables and no setters, nobody can change a card out from under anybody else.
Both facts are load-bearing in this chapter. In the previous chapter we defined a class to represent cards and used an array of Card objects to represent a deck. In this chapter we take additional steps toward object-oriented programming — starting by giving that array a class of its own.
Concept
Chapter 12 was arrays of objects — an array whose elements are Cards. Chapter 13 is objects of arrays: an object whose instance variable is an array. Wrapping the array in a class gives every deck operation a place to live.
Figure (svg): A Deck object containing a private cards array which holds references to Card objects
Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 13 (Objects of Arrays), Sections 13.1-13.6, pp. 217-224 — Chapter 13 opens on printed page 217.
Section
Section 13.1
Concept
Here is the beginning of a Deck class that encapsulates an array of Card objects. The instance variable is an array, and it is private — the same information-hiding decision as Lesson 11a.
public class Deck {
private Card[] cards;
public Deck(int n) {
this.cards = new Card[n];
}
public Card[] getCards() {
return this.cards;
}
}| member | kind | purpose |
|---|---|---|
| cards | private instance variable | the array of Cards |
| Deck(int n) | constructor | an empty deck of n slots |
| getCards() | getter | lets other classes read the array |
encapsulate — To wrap data inside a class so that the methods operating on it live alongside it and clients cannot reach it directly.
The constructor initialises the instance variable with an array of n cards, but it doesn't create any Card objects. That is Lesson 12b's null array, now hidden one level deeper.
Picture it
A Deck exists. Its array exists. There is not a single Card anywhere.
Figure (svg): A Deck object whose cards array holds only null references, representing an unpopulated deck
Two objects have been created — the Deck and the array — and the reference in each slot is null. This constructor is for building a deck you intend to fill yourself, which is exactly what subdeck will need in Section 13.5.
Worked example
We'll add another constructor that creates a standard 52-card array and populates it with Card objects. The body is Lesson 12b's nested loop, moved inside the class.
public Deck() {
this.cards = new Card[52];
int index = 0;
for (int suit = 0; suit <= 3; suit++) {
for (int rank = 1; rank <= 13; rank++) {
this.cards[index] = new Card(rank, suit);
index++;
}
}
}| constructor | argument | result |
|---|---|---|
| Deck(int n) | 5 | a deck of 5 nulls |
| Deck() | none | a full, ordered 52-card deck |
| how Java tells them apart | the parameter list | overloading, from Lesson 11a |
Two constructors, same name.
Why: Lesson 11a's overloading — Java picks by the parameter list.
The body is the code from Section 12.6.
Why: This method is similar to the example in Section 12.6; we just turned it into a constructor.
this.cards rather than a local.
Why: The array is stored in the instance variable, so it survives the constructor.
Using it is now one line.
Why: Deck deck = new Deck();
Verify: Create new Deck() and print the first and last cards; expect the Ace of Clubs and the King of Spades.
Why: The nested loop has not changed at all. What changed is where it lives: any program that wants a full deck now writes four characters instead of eight lines, which is the whole point of a constructor.
Prediction
Count the objects.
public Deck(int n) {
this.cards = new Card[n];
}
Deck sub = new Deck(5);| thing | how many |
|---|---|
| Deck objects | 1 |
| arrays | 1 |
| Card objects | ? |
Predict first
How many Card objects exist?
Correct: Zero — the array holds five nulls
Why: The constructor creates the array but never calls new Card, so every element is null. That is exactly what subdeck wants in Section 13.5 — an empty container it can fill with references to cards that already exist.
Concept
Now that we have a Deck class, we have a logical place to put methods that pertain to decks. Converting a static method to an instance method is Lesson 11b's three edits again.
public void print() {
for (Card card : this.cards) {
System.out.println(card);
}
}| as a static method | as an instance method | |
|---|---|---|
| declaration | static void printDeck(Card[] cards) | void print() |
| the array | a parameter | this.cards |
| invocation | printDeck(deck.getCards()) | deck.print() |
Notice that when we transform a static method into an instance method, the code is shorter. The parameter disappears because the object already carries the data — which is the argument for instance methods in one line.
Trap
getCards returns the reference, not a copy.
public Card[] getCards() {
return this.cards;
}
// so a client can do this:
Card[] c = deck.getCards();
c[0] = null; // the deck's own array is now damaged| the array is | consequence |
|---|---|
| private | clients cannot name it |
| returned by a getter | clients can reach it anyway |
| mutable | and can change it |
The private keyword protected the variable, not the object it refers to. This is Lesson 10a's aliasing, arriving through a method that looks perfectly innocent.
Know what a getter gives away, and decide deliberately.
// the book's choice - Pile.addDeck needs to iterate the cards
public Card[] getCards() {
return this.cards;
}
// the defensive alternative
public Card[] getCards() {
return Arrays.copyOf(this.cards, this.cards.length);
}| approach | clients can read | clients can damage the deck |
|---|---|---|
| return the array | yes | yes |
| return a copy | yes | no |
| the Cards themselves | readable either way | no — Card is immutable |
The last row is the saving grace. The Cards cannot be damaged whatever happens to the array, which is Lesson 12a's immutability paying off again — but the array's arrangement is still exposed.
Definition probe
Two constructors, two different needs.
Sort into buckets
Sort each situation.
Fill the middle
print traverses the deck's own array.
Fill in the blanks
public void print() this}.cards) card});
}
}
Why: As an instance method it reads this.cards rather than taking the array as a parameter, which is why the converted method is shorter than the static one. The enhanced for loop from Lesson 7a gives each element in turn, and println calls Card's toString automatically.
Explain it to yourself
The static version worked fine.
Discussion prompt
printDeck(Card[] cards) was a perfectly good static method. What does moving it into Deck actually improve?
Hint: Where does someone look for it?
Answer:
Discoverability. Someone with a Deck types deck. and sees every operation a deck supports. A loose static method in some other class has to be known about in advance.
And cohesion: the data and the code that operates on it sit together, so changing how a deck is stored means changing one file. The methods stop being scattered around the program.
Think Java's phrasing is we have a logical place to put methods that pertain to decks. The class is the place — which is most of what object-oriented programming means in practice.
Section
Section 13.2
Concept
For most card games you have to shuffle the deck; that is, put the cards in a random order. Section 7.6 showed how to generate random numbers, but it is not obvious how to use them to shuffle a deck — so start with a sketch.
public void shuffle() {
for each index i {
// choose a random number between i and length - 1
// swap the ith card and the randomly-chosen card
}
}| line | status |
|---|---|
| public void shuffle() { | real Java |
| for each index i { | English standing in for a for loop |
| // choose a random number... | a comment describing a step |
| // swap the ith card... | another step, not yet written |
pseudocode — A combination of Java statements and English comments used to outline an algorithm before writing it.
To outline this algorithm we'll use a combination of Java statements and English comments. This technique is sometimes called pseudocode. It compiles into nothing and it is still the most valuable thing you can write at this stage.
Notation
The obvious approach is to imitate people, and Think Java rejects it for a reason that is worth reading twice.
Annotate
This is a general trap worth naming. Copying how humans do something can copy the mechanics and lose the point — and here it produces code that is deterministic while looking random.
Worked example
The nice thing about pseudocode is that it often makes clear what other methods you are going to need. Read the two comments and the methods fall out.
private static int randomInt(int low, int high) {
// return a random number between low and high,
// including both
}
private void swapCards(int i, int j) {
// swap the ith and the jth cards in the array
}| the comment said | so we need | kind |
|---|---|---|
| choose a random number between i and length - 1 | randomInt(low, high) | static |
| swap the ith card and the chosen card | swapCards(i, j) | instance |
Read each comment as a request for a method.
Why: We need a method that chooses a random integer in a given range and a method that takes two indexes and swaps the cards at those positions.
Give each one a signature.
Why: Naming the parameters forces you to decide what it needs to know.
Leave the bodies empty for now.
Why: The design is settled before any of it is written.
Notice one is static and one is not.
Why: randomInt is a class method and swapCards is an instance method. Do you understand why?
Verify: Ask yourself the book's question before reading on: which of the two touches this.cards?
Why: swapCards rearranges the deck's own array, so it needs an object. randomInt just does arithmetic on two ints and touches no instance variable at all — so it does not need an object, and should not require one. That is the rule Section 13.8 will state outright.
Prediction
A program can divide and interleave perfectly.
// perfect riffle shuffle, applied eight times
// to a 52-card deck| shuffles | deck order |
|---|---|
| 1 | permuted |
| 8 | back to the original |
Predict first
What goes wrong?
Correct: A perfect shuffle is deterministic — eight of them restore the original order
Why: Human shuffling works because humans are imprecise; a program's precision turns the same operation into a fixed permutation whose period happens to be eight. The randomness in the human version came from the sloppiness, and the simulation drops exactly that.
Concept
Methods like randomInt and swapCards are called helper methods, because they help you solve parts of the problem.
helper method — A method that solves part of a larger problem, usually private because only methods in the same class need it.
top-down design — Writing pseudocode first, then writing the helper methods that make it work.
| property | reason |
|---|---|
| usually private | used only by methods in the class |
| small | each does one step of the outline |
| discovered, not planned | the pseudocode asked for them |
| testable alone | swapCards can be checked without shuffle |
The process of writing pseudocode first and then writing helper methods to make it work is a kind of top-down design. It is an alternative to incremental development and encapsulation and generalization, the other design processes in this book — Lessons 4b and 5b respectively. Three processes, and you choose.
Trap
Straight to Java, with every detail at once.
public void shuffle() {
for (int i = 0; i < cards.length; i++) {
int j = i + (int) (Math.random() * (cards.length - i));
Card temp = cards[i];
cards[i] = cards[j];
cards[j] = temp;
}
}| problem | why it hurts |
|---|---|
| the range arithmetic is inline | hard to check, easy to get wrong |
| the swap is inline | three lines of noise inside the real idea |
| nothing is reusable | selection sort needs a swap too |
| if it is wrong | you do not know which part |
It may well work. But the structure of the algorithm — traverse, choose, swap — is buried under arithmetic, and the swap that selection sort is about to need has to be written again.
Outline first; each comment becomes a method.
public void shuffle() {
for (int i = 0; i < cards.length; i++) {
int j = randomInt(i, cards.length - 1);
swapCards(i, j);
}
}| benefit | detail |
|---|---|
| shuffle reads as its own description | traverse, choose, swap |
| each helper is testable alone | check randomInt's range separately |
| swapCards is reused | selection sort needs exactly this |
| a bug has an address | you know which method to look in |
The finished method looks like the pseudocode — which is the sign that the decomposition was right. And swapCards is about to be reused by selection sort without a line being changed.
Definition probe
The deciding question is whether the method touches this.cards.
Sort into buckets
Sort each helper.
Fill the middle
Choose a random index and swap.
Fill in the blanks
for (int i = 0; i < cards.length; i++) randomInt}(i, cards.length - 1);
swapCards(i, j);
}
Why: The finished method is the pseudocode with each comment replaced by a call, which is the sign that the decomposition was right. Note the range: from i to the last index, so a card can be swapped with itself — excluding that possibility would actually make the shuffle less random.
Socratic
They work perfectly well as public methods.
Discussion prompt
Think Java says helper methods are often private. What does making swapCards private buy, given that a public version would behave identically?
Hint: What is a class promising with its public methods?
Answer:
A class's public methods are a promise: they are what other code is allowed to depend on. swapCards is a step in an algorithm, not a service the class offers.
Making it private means you can rename it, change its parameters, or delete it when the algorithm changes — without breaking anyone, because nobody outside could have called it.
This is Lesson 11a's argument again: the smaller the public surface, the more freedom you keep. Private is not about secrecy; it is about what you are allowed to change later.
Section
Section 13.3
Concept
Now that we have shuffled the deck, we need a way to put it back in order. There is an algorithm for sorting that is ironically similar to the algorithm for shuffling — it also traverses and swaps.
public void selectionSort() {
for each index i {
// find the lowest card at or to the right of i
// swap the ith card and the lowest card found
}
}
private int indexLowest(int low, int high) {
// find the lowest card between low and high
}| iteration | searches | swaps into |
|---|---|---|
| 0 | the whole array | position 0 |
| 1 | positions 1 to the end | position 1 |
| 2 | positions 2 to the end | position 2 |
| i | positions i to the end | position i |
selection sort — A sorting algorithm that traverses the array repeatedly, selecting the lowest remaining element each time and swapping it into place.
It's called selection sort because it works by traversing the array repeatedly and selecting the lowest (or highest) remaining card each time. During the ith iteration we find the lowest card to the right of i and swap it with the ith card.
Picture it
After each pass, one more position is permanently correct — and everything to its left will never move again.
Figure (svg): A row of array positions showing the sorted prefix growing one element per pass while the unsorted suffix shrinks
The searched region shrinks by one each pass, so the passes are not all the same length: the first examines n elements, the last examines two. That detail is what makes the total cost what it is.
Worked example
Again, the pseudocode helps with the design of the helper methods. And this time one of them already exists.
// from Section 13.2, unchanged:
private void swapCards(int i, int j) { ... }
// the one new helper this algorithm needs:
private int indexLowest(int low, int high) {
// find the lowest card between low and high
}| helper | written for | reused by |
|---|---|---|
| swapCards | shuffle | selectionSort |
| indexLowest | selectionSort | — |
| randomInt | shuffle | — |
Read the pseudocode's two comments.
Why: Find the lowest card at or to the right of i, then swap.
Notice the second one is already solved.
Why: We can reuse swapCards from the previous section, so we need only a method to find the lowest card.
Name the new one.
Why: indexLowest(int low, int high) — it returns an index, like the searches in Lesson 12b.
Notice it uses compareTo.
Why: Finding the lowest card means comparing cards, which is the method Lesson 12a wrote.
Verify: Shuffle a deck, sort it, and print it: expect a new deck's order — Clubs first, Aces low.
Why: The reuse is the payoff of the previous section's decomposition. Had the swap been written inline inside shuffle, it would now be written a second time inside selectionSort — and there would be two places for the same bug to live.
Prediction
Selection sort, first iteration, on a shuffled deck.
int lowest = indexLowest(0, cards.length - 1);
swapCards(0, lowest);| step | effect |
|---|---|
| indexLowest(0, 51) | the index of the lowest card |
| swapCards(0, that) | it moves to position 0 |
Predict first
What is true after the first pass?
Correct: cards[0] holds the lowest card in the whole deck, permanently
Why: The first pass searches the entire array, so what it finds is the overall lowest, and swapping it into position 0 puts it where it belongs for good. That is the invariant the algorithm maintains: after pass i, positions 0 through i are final and never searched again.
Concept
Selection sort is a simple algorithm, but it is not very efficient. The cost follows directly from the shape of the two loops.
// to sort n items:
// pass 1 examines n elements
// pass 2 examines n - 1
// ...
// pass n examines 1
//
// total ≈ n * n / 2, which is proportional to n squared| n | roughly n² / 2 comparisons |
|---|---|
| 10 | 50 |
| 52 | 1,350 |
| 1,000 | 500,000 |
| 1,000,000 | 500,000,000,000 |
To sort n items it has to traverse the array n − 1 times. Each traversal takes an amount of time proportional to n. The total time, therefore, is proportional to n². Read the last row: half a trillion comparisons to sort a million cards.
Trap
Restarting the search from zero each time.
for (int i = 0; i < cards.length; i++) {
int lowest = indexLowest(0, cards.length - 1); // always from 0
swapCards(i, lowest);
}| pass | finds | swaps into | result |
|---|---|---|---|
| 0 | the overall lowest | position 0 | correct |
| 1 | the overall lowest — again | position 1 | undoes pass 0 |
| 2 | the same card once more | position 2 | worse |
The lowest card gets dragged rightwards forever. The search must start at i, not at 0 — the region to the left is finished and must not be touched again.
Search only the unsorted region.
for (int i = 0; i < cards.length; i++) {
int lowest = indexLowest(i, cards.length - 1); // from i
swapCards(i, lowest);
}| pass | searches | positions now final |
|---|---|---|
| 0 | 0 to 51 | 0 |
| 1 | 1 to 51 | 0, 1 |
| 2 | 2 to 51 | 0, 1, 2 |
The invariant is that everything to the left of i is sorted and final. Naming that invariant is what makes the low bound obviously i — the same reasoning Lesson 8b used for accumulator loops.
Prediction
Selection sort, worst case, roughly.
// n - 1 traversals, each proportional to n
// total proportional to n squared| n | n² | roughly n² / 2 |
|---|---|---|
| 1,000 | 1,000,000 | 500,000 |
Predict first
Roughly how many comparisons?
Correct: About half a million
Why: Each of about a thousand passes examines up to a thousand elements, giving a million comparisons if you count generously and about half that once you account for the shrinking region. Either way the cost is proportional to n², which is why doubling the input roughly quadruples the work.
Matching
Three helpers across two algorithms.
Match the pairs
Why: swapCards is the one used by both shuffle and selectionSort — which is exactly the reuse that writing helper methods rather than inline code buys. Note that indexLowest returns a position, not a card, for the same reason Lesson 12b's searches did.
Counterexample
Quadratic sounds like a verdict.
Discussion prompt
Merge sort is asymptotically much better. Name a situation where selection sort is nevertheless a reasonable thing to use.
Hint: How big is a hand of cards?
Answer:
When n is small. For a hand of five cards the difference is nothing, and selection sort is shorter, simpler and easier to get right.
And when swaps are expensive but comparisons are cheap — selection sort performs at most n swaps, which is fewer than most alternatives.
Asymptotic cost is about large inputs, and it is worth remembering it says nothing about small ones. Think Java's own almostMergeSort uses selection sort on the two halves for exactly this reason before recursion replaces it.
Section
Section 13.4
Concept
We will develop a more efficient algorithm called merge sort. To sort n items, merge sort takes time proportional to n log₂ n. The idea behind it is one observation: if you have two decks, each of which has already been sorted, you can quickly merge them into a single sorted deck.
// try this with a real deck of cards:
// 1. form two decks of about 10 cards each and sort them,
// face up with the lowest cards on top
// 2. compare the TOP card from each deck and choose the lower;
// flip it over onto the merged deck
// 3. repeat until one deck is empty, then add the remaining cards| d1 top | d2 top | take | merged so far |
|---|---|---|---|
| 2♣ | 3♣ | 2♣ | 2♣ |
| 5♣ | 3♣ | 3♣ | 2♣ 3♣ |
| 5♣ | 7♣ | 5♣ | 2♣ 3♣ 5♣ |
| 9♣ | 7♣ | 7♣ | 2♣ 3♣ 5♣ 7♣ |
merge sort — A sorting algorithm that splits the collection in half, sorts each half, and merges the sorted halves.
You only ever look at the two top cards, and each comparison places one card permanently. Merging two sorted decks of ten cards takes about twenty comparisons, not two hundred.
Notation
Think Java gives the arithmetic outright, and it is worth reading slowly.
Annotate
Choosing an algorithm is a bigger decision than any amount of tuning. No amount of making selection sort's inner loop faster closes a factor of fifty thousand.
Worked example
Merge sort is three steps, and each one is a method you can write separately.
public Deck almostMergeSort() {
// divide the deck into two subdecks
// sort the subdecks using selectionSort
// merge the subdecks, return the result
}| step | the method needed | section |
|---|---|---|
| divide | subdeck(low, high) | 13.5 |
| sort each half | selectionSort | 13.3 |
| combine | merge(d1, d2) | 13.6 |
Split the deck in two.
Why: That needs a subdeck method, which is the next section.
Sort each half.
Why: For now, with the selection sort you already have.
Merge the two sorted halves.
Why: That needs a merge method, which is Section 13.6.
Notice this is still not merge sort.
Why: It is still not very efficient, because it uses selectionSort to sort the subdecks.
Verify: Work out why halving helps even with a quadratic sort: two sorts of 26 cards cost about 2 × 26² = 1,352 against 52² = 2,704 for one sort of 52.
Why: Halving alone nearly halves the work — and the obvious next thought is to halve again, which is what makes the method recursive in Lesson 13b. almostMergeSort is a deliberate stepping stone.
Prediction
Each comparison places one card.
// 20 cards total
// each comparison moves exactly one card to the result| cards to place | comparisons |
|---|---|
| 20 | at most 19 |
Predict first
Roughly how many comparisons does the merge need?
Correct: About 20
Why: Every comparison of the two top cards places exactly one card in the result, so twenty cards need at most nineteen comparisons — and fewer, since once a deck empties the rest are copied straight across. Merging is cheap; that cheapness is the whole basis of the algorithm.
Concept
Every row of this table is the same input sorted two different ways.
| n | selection sort ≈ n² | merge sort ≈ n log₂ n |
|---|---|---|
| 10 | 100 | 33 |
| 52 | 2,704 | 297 |
| 1,000 | 1,000,000 | 10,000 |
| 1,000,000 | 1,000,000,000,000 | 20,000,000 |
At n = 10 the difference barely matters. At a million it is the difference between a program that answers and one that does not — which is why the crossover point, not the formula, is what you actually care about in practice.
Trap
Merging assumes both inputs are already in order.
// d1 = 5♣ 2♣ 9♣ (NOT sorted)
// d2 = 3♣ 7♣ J♣
// compare tops: 5♣ vs 3♣ -> take 3♣
// compare tops: 5♣ vs 7♣ -> take 5♣
// compare tops: 2♣ vs 7♣ -> take 2♣ <- out of order!| merged so far | sorted? |
|---|---|
| 3♣ | yes |
| 3♣ 5♣ | yes |
| 3♣ 5♣ 2♣ | no |
Merging only looks at the two top cards, so it can never discover that something further down is out of place. The output is wrong and nothing complains — Lesson 12b's binary search precondition again.
Sort each half first — that is what the algorithm is.
// divide the deck into two subdecks
// SORT the subdecks
// merge the subdecks| step | guarantees |
|---|---|
| sort each subdeck | both inputs to merge are ordered |
| merge | comparing tops is enough |
| result | a fully sorted deck |
The middle step exists precisely to establish merge's precondition. That is worth seeing clearly, because it is what makes the recursion in Lesson 13b legitimate: whatever sorts the halves, merge only needs them sorted.
Definition probe
Two growth rates.
Sort into buckets
Sort each statement.
Fill the middle
Merge sort in outline.
Fill in the blanks
// divide the deck into two subdecks
// sort the subdecks
// merge the subdecks, return the result
Why: Divide, sort each half, merge. The middle step is what establishes merge's precondition — merge only compares the top two cards, so it is correct only when both inputs are already in order. Which sorting method the middle step uses is exactly the question Lesson 13b answers with recursion.
Explain it to yourself
You still have to sort all 52 cards.
Discussion prompt
Even with selection sort on both halves, splitting first is cheaper. Why does dividing the work reduce the total when nothing got faster?
Hint: What does squaring do to a halved input?
Answer:
Because the cost is quadratic. One sort of 52 costs about 52² = 2,704. Two sorts of 26 cost about 2 × 26² = 1,352 — half as much, plus a cheap merge.
Halving the input quarters the cost of a quadratic algorithm, and you do it twice, so you save half overall. The saving comes from the squaring, not from any speed-up.
And the argument repeats. If halving once helps, halving the halves helps again — which is exactly the thought that turns almostMergeSort into a recursive method in Lesson 13b.
Section
Sections 13.5-13.6
Concept
The first step of merge sort is to split the deck into two subdecks, each with about half of the cards. So we need a method that takes a range of indexes and returns a new deck with that subset.
public Deck subdeck(int low, int high) {
Deck sub = new Deck(high - low + 1);
for (int i = 0; i < sub.cards.length; i++) {
sub.cards[i] = this.cards[low + i];
}
return sub;
}| low | high | high - low + 1 | cards |
|---|---|---|---|
| 0 | 4 | 5 | indexes 0,1,2,3,4 |
| 0 | 25 | 26 | the first half |
| 26 | 51 | 26 | the second half |
| 3 | 3 | 1 | just card 3 |
The length of the subdeck is high − low + 1, because both the low card and the high card are included. Think Java says this plainly: forgetting the '+ 1' often leads to off-by-one errors, and drawing a picture is usually the best way to avoid them.
Picture it
The loop copies references, not cards. The result is a hand of five cards that are also still in the original deck.
Figure (svg): A subdeck array whose five references point at the same Card objects as the original deck's first five slots
The result is a hand with five cards that are shared with the original deck; that is, they are aliased. Two decks, one set of cards — and Lesson 10a said that was dangerous.
Worked example
Chapter 10 spent a whole section on why aliasing causes bugs. Chapter 13 aliases deliberately, and explains exactly why it is allowed to.
// from Lesson 12a:
public class Card {
private final int rank; // final - no method can change it
private final int suit;
// getters, no setters
}| question | answer |
|---|---|
| can a shared card be changed? | no — Card is immutable |
| so can one deck affect another? | no |
| what does sharing save? | creating 26 duplicate Card objects |
| would this be safe for a mutable class? | no |
State the general worry.
Why: Aliasing might not be a good idea, because changes to shared cards would be reflected in multiple decks.
Check whether it applies.
Why: But since Card objects are immutable, this kind of aliasing is not a problem.
Note the benefit.
Why: And it saves some memory because we don't create duplicate Card objects.
Note what would change the answer.
Why: If Card had setters, subdeck would have to copy each card — slower, and more code.
Verify: Take subdeck(0, 4), sort the subdeck, and confirm the original deck's first five cards are unchanged.
Why: The subdeck rearranges its own array of references; the Cards never move and never change. This is the design decision from Lesson 12a being cashed in — immutability is what makes sharing free, and it is why this method is four lines rather than fourteen.
Prediction
Both endpoints are included.
Deck sub = new Deck(high - low + 1);
// low = 0, high = 25| expression | value |
|---|---|
| high - low | 25 |
| high - low + 1 | 26 |
Predict first
How many cards does the subdeck hold?
Correct: 26
Why: Indexes 0 through 25 inclusive is twenty-six positions, because both endpoints count. This is the classic fence-post error, and the reliable check is the smallest case: subdeck(3, 3) must contain one card, and only the + 1 version gives that.
Concept
The next helper method we need is merge, which takes two sorted subdecks and returns a new deck containing all cards from both decks, in order.
private static Deck merge(Deck d1, Deck d2) {
// create a new deck, d3, big enough for all the cards
// use the index i to keep track of where we are at in
// the first deck, and the index j for the second deck
int i = 0;
int j = 0;
// the index k traverses the result deck
for (int k = 0; k < d3.length; k++) {
// if d1 is empty, use top card from d2
// if d2 is empty, use top card from d1
// otherwise, compare the top two cards
// add lowest card to the new deck at k
// increment i or j (depending on card)
}
// return the new deck
}| index | traverses |
|---|---|
| i | the first deck, d1 |
| j | the second deck, d2 |
| k | the result deck, d3 |
Three indexes, one per deck. k advances every iteration because a card is always placed; i and j advance only when a card is taken from their deck — which is the detail that makes merge work, and the reason Think Java warns it's a little tricky, so be sure to test it with different subdecks.
Trap
high - low loses a card.
Deck sub = new Deck(high - low); // one too small
// subdeck(0, 4) makes a 4-card deck for 5 cards
// subdeck(0, 25) makes a 25-card deck for 26 cards| low | high | cards in range | high - low | high - low + 1 |
|---|---|---|---|---|
| 0 | 4 | 5 | 4 — wrong | 5 — right |
| 0 | 25 | 26 | 25 — wrong | 26 — right |
| 3 | 3 | 1 | 0 — wrong | 1 — right |
The last row makes it obvious: a range from 3 to 3 contains one card, and high - low says zero. Checking a formula on the smallest case is the fastest way to catch this.
Both endpoints are included, so add one.
Deck sub = new Deck(high - low + 1);
for (int i = 0; i < sub.cards.length; i++) {
sub.cards[i] = this.cards[low + i];
}| i | reads from | writes to |
|---|---|---|
| 0 | cards[low] | sub.cards[0] |
| 1 | cards[low + 1] | sub.cards[1] |
| last | cards[high] | sub.cards[length - 1] |
Drawing a picture is usually the best way to avoid them, says the book — and the table above is that picture. Note also the offset low + i: the subdeck counts from 0 while the source counts from low.
Prediction
The subdeck shares cards with the original.
Deck sub = deck.subdeck(0, 4);
sub.selectionSort();
deck.print(); // the original| what moves | in which array |
|---|---|
| references | sub.cards only |
| Card objects | nothing moves — they are shared |
Predict first
What happens to the original deck?
Correct: Nothing — sorting rearranges the subdeck's own array of references
Why: The two decks share Card objects but each has its own array. Sorting moves references around inside sub.cards and never touches deck.cards or any Card. The aliasing would only matter if a Card could be modified — and Lesson 12a made sure none can.
Definition probe
merge keeps three of them.
Sort into buckets
Sort each index by when it advances.
Socratic
It could try to merge in place.
Discussion prompt
merge creates a third deck rather than rearranging the two it was given. What goes wrong if you try to do it in place?
Hint: Where would a card go if its slot is occupied?
Answer:
There is nowhere to put a card. Placing d2's top card at the front of d1 means shifting every card in d1 along — which turns a cheap merge into a quadratic one.
The third deck gives every card a free slot, so each is placed in one step and merge stays proportional to the number of cards.
Merge sort buys its speed with memory — it needs room for a copy. That is a trade worth naming, and it is why some sorting algorithms that use no extra space are preferred in memory-tight settings despite being harder to write.
Comparison
Fill the blanks.
Comparison matrix
| selection sort | merge sort | |
|---|---|---|
| how it works | select the lowest, swap it into place | split, sort the halves, merge |
| cost for n items | proportional to n² | proportional to n log₂ n |
| steps for a million items | about a trillion | about 20 million |
| extra memory needed | none | room for a copy |
| helper methods | indexLowest, swapCards | subdeck, merge |
The fourth row is the one that keeps selection sort alive. Merge sort's speed is bought with memory, and for a small deck the trade is not obviously worth making.
Pattern
Top-down design: write the outline, and let it tell you which methods to write.
// 1. write the algorithm as pseudocode
public void shuffle() {
for each index i {
// choose a random number between i and length - 1
// swap the ith card and the randomly-chosen card
}
}
// 2. each comment becomes a helper method signature
private static int randomInt(int low, int high) { }
private void swapCards(int i, int j) { }
// 3. fill in the helpers, then the outline becomes real code
public void shuffle() {
for (int i = 0; i < cards.length; i++) {
swapCards(i, randomInt(i, cards.length - 1));
}
}| design process | when to reach for it | lesson |
|---|---|---|
| incremental development | you are not sure the pieces work | 4b |
| encapsulation and generalization | you have working code to tidy | 5b |
| top-down design | you know the shape but not the pieces | 13a |
Check
Work it out before you click.
public Deck(int n) {
this.cards = new Card[n];
}
Deck sub = new Deck(26);| created | count |
|---|---|
| Deck objects | 1 |
| arrays | 1 |
Check your understanding
How many Card objects has this created?
Answer: A
Why: The constructor allocates the array and stops; nothing calls new Card. That is exactly what subdeck needs — an empty container to fill with references to cards that already exist. Creating fresh Cards there would be both wasteful and wrong, since the subdeck is meant to share.
new Deck() produces; this constructor makes exactly n slots.Check
Work it out before you click.
Deck sub = new Deck(high - low + 1);
// subdeck(3, 3)| low | high | cards in range |
|---|---|---|
| 3 | 3 | 1 |
Check your understanding
Why is the + 1 necessary?
Answer: A
Why: The range includes the card at low and the card at high, so subdeck(3, 3) must hold one card — and high - low gives zero. Checking a formula against its smallest case is the quickest way to catch this kind of off-by-one, and Think Java recommends drawing a picture for the same reason.
Check
Work it out before you click.
// a million items
// selection sort: proportional to n^2
// merge sort: proportional to n log2 n
// log2 of a million is about 20| algorithm | steps |
|---|---|
| merge sort | ? |
| selection sort | ? |
Check your understanding
Roughly how many steps does each need?
Answer: A
Why: n log₂ n for a million is 1,000,000 × 20 = 20 million; n² is 1,000,000 × 1,000,000 = one trillion. That factor of fifty thousand is why the choice of algorithm dominates every other optimisation — no amount of tuning selection sort's inner loop closes it.
Real world
Writing the outline before the code is a habit that professionals keep.
Discussion prompt
Think Java introduces pseudocode as a way to design shuffle. Why do experienced programmers still write it — and where else does the same idea appear?
Hint: What is easy to change on paper and expensive to change in code?
Answer:
Because the outline is where the design decisions are, and it is cheap to change. Rewriting five comments costs a minute; rewriting five methods costs an afternoon.
The same idea appears as a design document before a project, a plan in a comment before a hard method, and a commit message written before the commit — each is an outline that exposes a bad plan early.
And it tells you what to build. The two comments in shuffle's pseudocode named randomInt and swapCards, and one of those turned out to be exactly what selection sort needed as well — a reuse nobody planned, discovered because the step had a name.
Commit first
Commit to an answer and to your confidence.
Predict first
subdeck copies references, so the subdeck and the original share Card objects. Why is that not a bug?
Correct: Because Card is immutable, so a shared card cannot be changed by either deck
Why: Think Java raises the worry explicitly — aliasing might not be a good idea, because changes to shared cards would be reflected in multiple decks — and then dismisses it: but since Card objects are immutable, this kind of aliasing is not a problem. The final instance variables and missing setters from Lesson 12a are what make this four-line method correct. Each deck still has its own array, so rearranging one leaves the other alone; only the Cards are shared, and they never change.
Explain it
Two minutes, out loud.
Discussion prompt
A classmate has written selection sort and asks why anyone would bother with merge sort, since both produce a sorted deck. Give them the argument with the numbers, and say what merge sort costs.
Hint: One trillion against twenty million, and memory.
Answer:
Both are correct — that is not the question. Selection sort's work is proportional to n², so a million items takes about a trillion steps. Merge sort is n log n, and log₂ of a million is about twenty, so it takes about twenty million.
For 52 cards you would not notice. The gap only opens as n grows, and then it opens enormously — a factor of fifty thousand at a million.
What it costs is memory: merge needs a new deck to write into, so merge sort needs room for a copy while selection sort sorts in place. Naming the cost is what makes it an argument rather than a slogan.
Exit ticket
One question before you close the deck.
Predict first
What is the point of writing pseudocode before writing the method?
Correct: It settles the algorithm's structure and reveals which helper methods you need, while changes are still cheap
Why: Think Java's phrasing is that the nice thing about pseudocode is that it often makes clear what other methods you are going to need. The two comments inside shuffle's outline named randomInt and swapCards before either existed — and swapCards turned out to be exactly what selection sort needed too. Rewriting a comment costs nothing; rewriting a method costs an afternoon, which is why the outline comes first.
Connect it up
One page, from memory.
Draw it
Draw a Deck object with its private cards array pointing at Card objects, then draw a subdeck beside it whose references point at the same Cards — and write one sentence saying why that is safe. Beneath it, write shuffle's pseudocode and the two helper signatures it produced, marking which is static and why. Finish with the two cost formulas, n² and n log₂ n, and the two numbers for a million items.
Recap
Six sections that wrap an array in a class and then compare two ways of sorting it.
| if you remember one thing | it is this |
|---|---|
| about design | write the outline first; it names the methods |
| about sorting | n² and n log n differ by fifty thousand at a million |
| about aliasing | immutability is what makes sharing free |
Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.